From 55f50029e94670e533abe921f64d2efc0fc3d584 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 6 Apr 2017 22:40:03 -0400 Subject: [PATCH] Route validation Route value validation Adjust validation Unit tests --- .../commons/cloudfoundry/client/CFRoute.java | 22 + .../cloudfoundry/client/CFRouteBuilder.java | 237 +++++++ .../cloudfoundry/client/CFRouteImpl.java | 92 +++ .../cloudfoundry/client/CFRouteTest.java | 578 ++++++++++++++++++ .../reconcile/ReconcileException.java | 5 + .../commons/util/ValueParseException.java | 17 + .../SchemaBasedYamlASTReconciler.java | 28 +- .../commons/cloudfoundry/client/CFRoute.java | 22 + .../cloudfoundry/client/CFRouteBuilder.java | 238 ++++++++ .../cloudfoundry/client/CFRouteImpl.java | 92 +++ .../cloudfoundry/client/CFRouteTests.java | 578 ++++++++++++++++++ .../yaml/ManifestYamlSchemaProblemsTypes.java | 4 +- .../manifest/yaml/ManifestYmlSchema.java | 4 +- .../manifest/yaml/RouteValueParser.java | 52 ++ .../manifest/yaml/ManifestYamlEditorTest.java | 87 +++ 15 files changed, 2052 insertions(+), 4 deletions(-) create mode 100644 headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java create mode 100644 headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java create mode 100644 headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java create mode 100644 headless-services/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTest.java create mode 100644 vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java create mode 100644 vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java create mode 100644 vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java create mode 100644 vscode-extensions/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTests.java create mode 100644 vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java new file mode 100644 index 000000000..a91f433e3 --- /dev/null +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java @@ -0,0 +1,22 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +public interface CFRoute { + + int NO_PORT = -1; + String EMPTY_ROUTE = ""; + + public static CFRouteBuilder builder() { + return new CFRouteBuilder(); + } + + String getDomain(); + + String getHost(); + + String getPath(); + + int getPort(); + + String getRoute(); + +} diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java new file mode 100644 index 000000000..c11524989 --- /dev/null +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java @@ -0,0 +1,237 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.operations.routes.Route; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; + +public class CFRouteBuilder { + private String domain; + private String host; + private String path; + private int port = CFRoute.NO_PORT; + private String fullRoute; + + public CFRoute build() { + return new CFRouteImpl(this.domain, this.host, this.path, this.port, this.fullRoute); + } + + public CFRouteBuilder domain(String domain) { + this.domain = domain; + // may seem like the more ideal place is to build the full route when + // building the route, rather than repeating + // the process each time a domain, host, path or port value is set + // but the "from" option should be allowed to overwrite the full route + // as well since it already + // has the full value. Therefore re-construct the full value if the + // route is being built piece by piece, but not in from + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder host(String host) { + this.host = host; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder path(String path) { + this.path = path; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder port(int port) { + this.port = port; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder from(Route route) { + // Route doesn't seem to have API to get a port + this.port = CFRoute.NO_PORT; + this.domain = route.getDomain(); + this.host = route.getHost(); + this.path = route.getPath(); + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + /** + * Builds a {@link CFRoute} given a desiredUrl. This does NOT validate, and + * will attempt to build a route the best way it can given the desiredUrl. + * External components, like the CF Java client, can then validate the + * CFRoute. + * + * @param desiredUrl + * @param domains + * @return this builder + */ + public CFRouteBuilder from(String desiredUrl, Collection domains) { + + + //If it is empty or null, there is nothing to build. However, be sure that the + // full route value is non-null, even if the "components" may be null + if (!StringUtil.hasText(desiredUrl)) { + this.fullRoute = CFRoute.EMPTY_ROUTE; + return this; + } else { + // Be sure to set the full route. + this.fullRoute = desiredUrl; + } + + // Based on CLI cf/actors/routes.go and testing CLI directly with + // different "routes" values: + // 1. Paths is not allowed in TCP route (valid TCP route: + // "tcp.spring.io:8888") + // 2. Ports are not allowed in HTTP route (valid HTTP route: + // "myapps.cfapps.io/pathToApp/home") + // 3. Schemes (e.g. "http://") are not allowed in routes values. + // Anything that has a ":" is assumed to be TCP route followed by a port + // 4. Route can just be domain, or host and domain + // + // Therefore, routes values cannot be treated as URIs or URLs, but a + // combination of domain, host, path and port + // NOTE: The validation above doesn't need to take place here. The + // client or CF will validate correct combinations of routes. + // However, We may want to implement similar + // validation to the CF manifest editor though. + + String matchedHost = null; + String hostAndDomain = null; + + // Split into hostDomain segment, port and path + int slashIndex = desiredUrl.indexOf('/'); + if (slashIndex >= 0) { + hostAndDomain = desiredUrl.substring(0, slashIndex); + String tempPath = desiredUrl.substring(slashIndex); + // Do not set empty strings. If there is no path, then it should be + // null + if (StringUtil.hasText(tempPath)) { + this.path = tempPath; + } + } else { + hostAndDomain = desiredUrl; + } + + // CF Route builder does not validate, so don't allow exceptions to + // prevent parsing of the route. The builder should attempt to build + // a route the best way it can, even if it may have invalid information. + // This allows external participants, like the CF Java client, to + // perform validation + try { + String[] portSegments = hostAndDomain.split(":"); + if (portSegments.length == 2) { + hostAndDomain = portSegments[0]; + this.port = Integer.parseInt(portSegments[1]); + } + } catch (NumberFormatException e) { + Log.log(e); + } + + this.domain = findDomain(hostAndDomain, domains); + + if (this.domain != null) { + matchedHost = hostAndDomain.substring(0, hostAndDomain.length() - this.domain.length()); + if (matchedHost.endsWith(".")) { + matchedHost = matchedHost.substring(0, matchedHost.length() - 1); + } + + // Don't set empty strings + if (StringUtil.hasText(matchedHost)) { + this.host = matchedHost; + } + } else { + // Do a basic split on '.', where first segment is the host, and the + // rest domain + int firstDotIndex = hostAndDomain.indexOf('.'); + if (firstDotIndex >= 0) { + String tempDomain = hostAndDomain.substring(firstDotIndex + 1); + // Don't set empty strings + if (StringUtil.hasText(tempDomain)) { + this.domain = tempDomain; + } + + String tempHost = hostAndDomain.substring(0, firstDotIndex); + if (StringUtil.hasText(tempHost)) { + this.host = tempHost; + } + } else { + if (StringUtil.hasText(hostAndDomain)) { + this.host = hostAndDomain; + } + } + } + + return this; + } + + public static String findDomain(String hostDomain, Collection domains) { + if (hostDomain == null) { + return null; + } + // find exact match + for (String name : domains) { + if (hostDomain.equals(name)) { + return hostDomain; + } + } + // Otherwise split on the first "." and try again + if (hostDomain.indexOf(".") >= 0 && hostDomain.indexOf(".") + 1 < hostDomain.length()) { + String remaining = hostDomain.substring(hostDomain.indexOf(".") + 1, hostDomain.length()); + return findDomain(remaining, domains); + } else { + return null; + } + } + + /** + * A basic building of a full route value. It performs no validation, just + * builds based on whether the parameter are set + * + * @param host + * @param domain + * @param path + * @param port + * @return Route value build with the given components. Always returns a non-null route. Empty route if no arguments are passed. + */ + public static String buildRouteVal(String host, String domain, String path, int port) { + + StringBuilder builder = new StringBuilder(); + if (StringUtil.hasText(host)) { + builder.append(host); + } + + if (StringUtil.hasText(domain)) { + if (StringUtil.hasText(host)) { + builder.append('.'); + } + builder.append(domain); + } + + if (port != CFRoute.NO_PORT) { + builder.append(':'); + builder.append(Integer.toString(port)); + } + + if (StringUtil.hasText(path)) { + if (!path.startsWith("/")) { + builder.append('/'); + } + builder.append(path); + } + + return builder.toString(); + } + + public CFRouteBuilder from(String desiredUrl, List cloudDomains) { + List domains = cloudDomains + .stream() + .map(CFDomain::getName) + .collect(Collectors.toList()); + return from(desiredUrl, domains); + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java new file mode 100644 index 000000000..5a5f1d4e0 --- /dev/null +++ b/headless-services/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java @@ -0,0 +1,92 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +class CFRouteImpl implements CFRoute { + + final private String domain; + final private String host; + final private String path; + final private int port; + final private String fullRoute; + + CFRouteImpl(String domain, String host, String path, int port, String fullRoute) { + super(); + this.domain = domain; + this.host = host; + this.path = path; + this.port = port; + this.fullRoute = fullRoute; + } + + public String getDomain() { + return domain; + } + + public String getHost() { + return host; + } + + public String getPath() { + return path; + } + + public int getPort() { + return port; + } + + public String getRoute() { + return fullRoute; + } + + @Override + public String toString() { + return "CFRoute [domain=" + domain + ", host=" + host + ", path=" + path + ", port=" + port +"]"; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((domain == null) ? 0 : domain.hashCode()); + result = prime * result + ((fullRoute == null) ? 0 : fullRoute.hashCode()); + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + port; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFRouteImpl other = (CFRouteImpl) obj; + if (domain == null) { + if (other.domain != null) + return false; + } else if (!domain.equals(other.domain)) + return false; + if (fullRoute == null) { + if (other.fullRoute != null) + return false; + } else if (!fullRoute.equals(other.fullRoute)) + return false; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + if (port != other.port) + return false; + return true; + } + +} diff --git a/headless-services/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTest.java b/headless-services/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTest.java new file mode 100644 index 000000000..e27c446f2 --- /dev/null +++ b/headless-services/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTest.java @@ -0,0 +1,578 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class CFRouteTest { + + public static final List SPRING_CLOUD_DOMAINS = Arrays.asList("springsource.org", "spring.io", + "myowndomain.spring.io", "tcp.spring.io", "spring.framework"); + + + @Test + public void test_domain_host() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_domain_only() throws Exception { + CFRoute route = CFRoute.builder().from("spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_match() throws Exception { + CFRoute route = CFRoute.builder().from("myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting() throws Exception { + // For domains that do not exist, the first segment is assumed to be the "host" + CFRoute route = CFRoute.builder().from("app.doesnotexist.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path_port() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io:60100/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(60100, route.getPort()); + Assert.assertEquals("app.doesnotexist.io:60100/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_match_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_domain_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath", route.getRoute()); + } + + @Test + public void test_domain_host_path_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath/additionalSegment", SPRING_CLOUD_DOMAINS) + .build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath/additionalSegment", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath/additionalSegment", route.getRoute()); + } + + @Test + public void test_tcp_port() throws Exception { + CFRoute route = CFRoute.builder().from("tcp.spring.io:9000", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("tcp.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(9000, route.getPort()); + Assert.assertEquals("tcp.spring.io:9000", route.getRoute()); + } + + @Test + public void test_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("justhost/path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals("justhost/path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_routes() throws Exception { + // A CFRoute does not validate route values. It can create any CF route even with wrong domains + // ports, hosts.. This tests that the route builder is parsing an invalid route into different + // components that some other external mechanism (like the CF Java client) can the use to validate + + CFRoute route = CFRoute.builder().from("", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(null, SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost.", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost.",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("..justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals(".justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("..justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("/justpath/morepath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/justpath/morepath",route.getPath()); + Assert.assertEquals("/justpath/morepath",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_incorrect_ports() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io:notAn1nt3g3r", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp.spring.io:notAn1nt3g3r",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + // Test parsing around the first encountered ':' + route = CFRoute.builder().from("http://myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("http://myapp.spring.io",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("tcp.spring.io:8000:9000", SPRING_CLOUD_DOMAINS).build(); + // Only one ':' is allowed. it should not be able to parse a port if more than ':' is encountered + Assert.assertEquals("tcp.spring.io:8000:9000",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("myapp.spring.io:8000/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io:8000/",route.getRoute()); + Assert.assertEquals(8000, route.getPort()); + } + + @Test + public void test_incorrect_paths() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io//path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("//path",route.getPath()); + Assert.assertEquals("myapp.spring.io//path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("myapp.spring.io/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void parse_null_domain() throws Exception { + String domain = CFRouteBuilder.findDomain("", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(null, SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("...", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void parse_valid_domain() throws Exception { + + // These exist + String domain = CFRouteBuilder.findDomain("spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain(".spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("..spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + } + + @Test + public void parse_invalid_domain() throws Exception { + + // These variations of existing domains don't exist + String domain = CFRouteBuilder.findDomain("spring.io.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.cfapps.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.io.cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown.domain.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void bug_142279275_parse_hostAndPathSameName() throws Exception { + + // Fixes Pivotal Tracker item 142279275 + CFRoute route = CFRoute.builder().from("hello-user.myowndomain.spring.io/hello", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("hello-user", route.getHost()); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("/hello", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("hello-user.myowndomain.spring.io/hello", route.getRoute()); + } + + @Test + public void build_route_value_empty() throws Exception { + + String val = CFRouteBuilder.buildRouteVal(null, null, null, CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + val = CFRouteBuilder.buildRouteVal("", "", "", CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + } + + @Test + public void build_route_value() throws Exception { + + String val = CFRouteBuilder.buildRouteVal("appHost", null, null, CFRoute.NO_PORT); + Assert.assertEquals("appHost", val); + + val = CFRouteBuilder.buildRouteVal(null, "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", 8000); + Assert.assertEquals(":8000/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, null, 60101); + Assert.assertEquals(":60101", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", 60101); + Assert.assertEquals("appHost.cfapps.io:60101/path/to/app", val); + } + + @Test + public void test_build_route_from_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_nonexisting_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("not.exist.io").build(); + Assert.assertEquals("not.exist.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("not.exist.io", route.getRoute()); + } + + @Test + public void test_build_route_from_null_domain() throws Exception { + CFRoute route = CFRoute.builder().domain(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("").build(); + Assert.assertEquals("",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + } + + @Test + public void test_build_route_from_host() throws Exception { + CFRoute route = CFRoute.builder().host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_null_host() throws Exception { + CFRoute route = CFRoute.builder().host(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertNull(route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_host() throws Exception { + CFRoute route = CFRoute.builder().host("").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("",route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_domain_host() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_path() throws Exception { + CFRoute route = CFRoute.builder().path("/path").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path", route.getRoute()); + } + + @Test + public void test_build_route_from_path_2() throws Exception { + CFRoute route = CFRoute.builder().path("/path/additional").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/additional",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/additional", route.getRoute()); + } + + @Test + public void test_build_route_from_path_3() throws Exception { + CFRoute route = CFRoute.builder().path("/path/").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_samename() throws Exception { + CFRoute route = CFRoute.builder().path("/myapp").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/myapp",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").port(8000).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("spring.io:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_port() throws Exception { + CFRoute route = CFRoute.builder().port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals(":8000", route.getRoute()); + } + + + @Test + public void test_build_route_from_no_port() throws Exception { + CFRoute route = CFRoute.builder().port(CFRoute.NO_PORT).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_no_port_2() throws Exception { + CFRoute route = CFRoute.builder().port(-1).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_tcp_port_building() throws Exception { + CFRoute route = CFRoute.builder().domain("tcp.spring.io").port(8000).build(); + Assert.assertEquals("tcp.spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("tcp.spring.io:8000", route.getRoute()); + } + + + @Test + public void test_complete() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").path("/mypath/additional").port(8000).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/mypath/additional", route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp.spring.io:8000/mypath/additional", route.getRoute()); + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ReconcileException.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ReconcileException.java index de45b2ec6..701cee6bd 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ReconcileException.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ReconcileException.java @@ -28,6 +28,11 @@ public class ReconcileException extends ValueParseException implements ProblemTy this.problemType = problemType; } + public ReconcileException(String message, ProblemType problemType, int start, int end) { + super(message, start, end); + this.problemType = problemType; + } + @Override public ProblemType getProblemType() { return problemType; diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParseException.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParseException.java index c4da05427..8e4d0ffaf 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParseException.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParseException.java @@ -18,6 +18,9 @@ package org.springframework.ide.vscode.commons.util; */ public class ValueParseException extends Exception { + private int startIndex = -1; + private int endIndex = -1; + /** * */ @@ -26,5 +29,19 @@ public class ValueParseException extends Exception { public ValueParseException(String message) { super(message); } + + public ValueParseException(String message, int startIndex, int endIndex) { + this(message); + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + public int getStartIndex() { + return startIndex; + } + + public int getEndIndex() { + return endIndex; + } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index 99d8dbff1..a76cf194b 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -179,8 +179,21 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { } } catch (Exception e) { ProblemType problemType = getProblemType(e); + DocumentRegion region = new DocumentRegion(doc, node.getStartMark().getIndex(), node.getEndMark().getIndex()); + if (e instanceof ValueParseException) { + ValueParseException parseException = (ValueParseException) e; + int start = parseException.getStartIndex() >= 0 + ? Math.min(node.getStartMark().getIndex() + parseException.getStartIndex(), + node.getEndMark().getIndex()) + : node.getStartMark().getIndex(); + int end = parseException.getEndIndex() >= 0 + ? Math.min(node.getStartMark().getIndex() + parseException.getEndIndex(), + node.getEndMark().getIndex()) + : node.getEndMark().getIndex(); + region = new DocumentRegion(doc, start, end); + } String msg = getMessage(e); - valueParseError(type, node, msg, problemType); + valueParseError(type, region, msg, problemType); } } } else { @@ -192,7 +205,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { } } } - + private String getMessage(Exception _e) { Throwable e = ExceptionUtil.getDeepestCause(_e); @@ -328,6 +341,13 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { problem(node, parseErrorMsg, problemType); } + private void valueParseError(YType type, DocumentRegion region, String parseErrorMsg, ProblemType problemType) { + if (!StringUtil.hasText(parseErrorMsg)) { + parseErrorMsg= "Couldn't parse as '"+describe(type)+"'"; + } + problem(region, parseErrorMsg, problemType); + } + private void unknownBeanProperty(Node keyNode, YType type, String name) { problem(keyNode, "Unknown property '"+name+"' for type '"+typeUtil.niceTypeName(type)+"'"); } @@ -385,6 +405,10 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { problems.accept(YamlSchemaProblems.problem(problemType, msg, node)); } + private void problem(DocumentRegion region, String msg, ProblemType problemType) { + problems.accept(YamlSchemaProblems.problem(problemType, msg, region)); + } + private void problem(DocumentRegion region, String msg) { problems.accept(YamlSchemaProblems.schemaProblem(msg, region)); } diff --git a/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java new file mode 100644 index 000000000..097b7f094 --- /dev/null +++ b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRoute.java @@ -0,0 +1,22 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +public interface CFRoute { + + int NO_PORT = -1; + String EMPTY_ROUTE = ""; + + static CFRouteBuilder builder() { + return new CFRouteBuilder(); + } + + String getDomain(); + + String getHost(); + + String getPath(); + + int getPort(); + + String getRoute(); + +} diff --git a/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java new file mode 100644 index 000000000..3a572b34b --- /dev/null +++ b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteBuilder.java @@ -0,0 +1,238 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.cloudfoundry.operations.routes.Route; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; + +public class CFRouteBuilder { + private String domain; + private String host; + private String path; + private int port = CFRoute.NO_PORT; + private String fullRoute; + + public CFRoute build() { + return new CFRouteImpl(this.domain, this.host, this.path, this.port, this.fullRoute); + } + + public CFRouteBuilder domain(String domain) { + this.domain = domain; + // may seem like the more ideal place is to build the full route when + // building the route, rather than repeating + // the process each time a domain, host, path or port value is set + // but the "from" option should be allowed to overwrite the full route + // as well since it already + // has the full value. Therefore re-construct the full value if the + // route is being built piece by piece, but not in from + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder host(String host) { + this.host = host; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder path(String path) { + this.path = path; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder port(int port) { + this.port = port; + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + public CFRouteBuilder from(Route route) { + // Route doesn't seem to have API to get a port + this.port = CFRoute.NO_PORT; + this.domain = route.getDomain(); + this.host = route.getHost(); + this.path = route.getPath(); + this.fullRoute = buildRouteVal(this.host, this.domain, this.path, this.port); + return this; + } + + /** + * Builds a {@link CFRoute} given a desiredUrl. This does NOT validate, and + * will attempt to build a route the best way it can given the desiredUrl. + * External components, like the CF Java client, can then validate the + * CFRoute. + * + * @param desiredUrl + * @param domains + * @return this builder + */ + public CFRouteBuilder from(String desiredUrl, Collection domains) { + + + //If it is empty or null, there is nothing to build. However, be sure that the + // full route value is non-null, even if the "components" may be null + if (!StringUtil.hasText(desiredUrl)) { + this.fullRoute = CFRoute.EMPTY_ROUTE; + return this; + } else { + // Be sure to set the full route. + this.fullRoute = desiredUrl; + } + + // Based on CLI cf/actors/routes.go and testing CLI directly with + // different "routes" values: + // 1. Paths is not allowed in TCP route (valid TCP route: + // "tcp.spring.io:8888") + // 2. Ports are not allowed in HTTP route (valid HTTP route: + // "myapps.cfapps.io/pathToApp/home") + // 3. Schemes (e.g. "http://") are not allowed in routes values. + // Anything that has a ":" is assumed to be TCP route followed by a port + // 4. Route can just be domain, or host and domain + // + // Therefore, routes values cannot be treated as URIs or URLs, but a + // combination of domain, host, path and port + // NOTE: The validation above doesn't need to take place here. The + // client or CF will validate correct combinations of routes. + // However, We may want to implement similar + // validation to the CF manifest editor though. + + String matchedHost = null; + String hostAndDomain = null; + + // Split into hostDomain segment, port and path + int slashIndex = desiredUrl.indexOf('/'); + if (slashIndex >= 0) { + hostAndDomain = desiredUrl.substring(0, slashIndex); + String tempPath = desiredUrl.substring(slashIndex); + // Do not set empty strings. If there is no path, then it should be + // null + if (StringUtil.hasText(tempPath)) { + this.path = tempPath; + } + } else { + hostAndDomain = desiredUrl; + } + + // CF Route builder does not validate, so don't allow exceptions to + // prevent parsing of the route. The builder should attempt to build + // a route the best way it can, even if it may have invalid information. + // This allows external participants, like the CF Java client, to + // perform validation + try { + String[] portSegments = hostAndDomain.split(":"); + if (portSegments.length == 2) { + hostAndDomain = portSegments[0]; + this.port = Integer.parseInt(portSegments[1]); + } + } catch (NumberFormatException e) { + Log.log(e); + } + + this.domain = findDomain(hostAndDomain, domains); + + if (this.domain != null) { + matchedHost = hostAndDomain.substring(0, hostAndDomain.length() - this.domain.length()); + if (matchedHost.endsWith(".")) { + matchedHost = matchedHost.substring(0, matchedHost.length() - 1); + } + + // Don't set empty strings + if (StringUtil.hasText(matchedHost)) { + this.host = matchedHost; + } + } else { + // Do a basic split on '.', where first segment is the host, and the + // rest domain + int firstDotIndex = hostAndDomain.indexOf('.'); + if (firstDotIndex >= 0) { + String tempDomain = hostAndDomain.substring(firstDotIndex + 1); + // Don't set empty strings + if (StringUtil.hasText(tempDomain)) { + this.domain = tempDomain; + } + + String tempHost = hostAndDomain.substring(0, firstDotIndex); + if (StringUtil.hasText(tempHost)) { + this.host = tempHost; + } + } else { + if (StringUtil.hasText(hostAndDomain)) { + this.host = hostAndDomain; + } + } + } + + return this; + } + + public static String findDomain(String hostDomain, Collection domains) { + if (hostDomain == null) { + return null; + } + // find exact match + for (String name : domains) { + if (hostDomain.equals(name)) { + return hostDomain; + } + } + // Otherwise split on the first "." and try again + if (hostDomain.indexOf(".") >= 0 && hostDomain.indexOf(".") + 1 < hostDomain.length()) { + String remaining = hostDomain.substring(hostDomain.indexOf(".") + 1, hostDomain.length()); + return findDomain(remaining, domains); + } else { + return null; + } + } + + /** + * A basic building of a full route value. It performs no validation, just + * builds based on whether the parameter are set + * + * @param host + * @param domain + * @param path + * @param port + * @return Route value build with the given components. Always returns a non-null route. Empty route if no arguments are passed. + */ + public static String buildRouteVal(String host, String domain, String path, int port) { + + StringBuilder builder = new StringBuilder(); + if (StringUtil.hasText(host)) { + builder.append(host); + } + + if (StringUtil.hasText(domain)) { + if (StringUtil.hasText(host)) { + builder.append('.'); + } + builder.append(domain); + } + + if (port != CFRoute.NO_PORT) { + builder.append(':'); + builder.append(Integer.toString(port)); + } + + if (StringUtil.hasText(path)) { + if (!path.startsWith("/")) { + builder.append('/'); + } + builder.append(path); + } + + return builder.toString(); + } + + public CFRouteBuilder from(String desiredUrl, List cloudDomains) { + List domains = cloudDomains + .stream() + .map(CFDomain::getName) + .collect(Collectors.toList()); + return from(desiredUrl, domains); + } + +} \ No newline at end of file diff --git a/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java new file mode 100644 index 000000000..5a5f1d4e0 --- /dev/null +++ b/vscode-extensions/commons/commons-cf/src/main/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteImpl.java @@ -0,0 +1,92 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +class CFRouteImpl implements CFRoute { + + final private String domain; + final private String host; + final private String path; + final private int port; + final private String fullRoute; + + CFRouteImpl(String domain, String host, String path, int port, String fullRoute) { + super(); + this.domain = domain; + this.host = host; + this.path = path; + this.port = port; + this.fullRoute = fullRoute; + } + + public String getDomain() { + return domain; + } + + public String getHost() { + return host; + } + + public String getPath() { + return path; + } + + public int getPort() { + return port; + } + + public String getRoute() { + return fullRoute; + } + + @Override + public String toString() { + return "CFRoute [domain=" + domain + ", host=" + host + ", path=" + path + ", port=" + port +"]"; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((domain == null) ? 0 : domain.hashCode()); + result = prime * result + ((fullRoute == null) ? 0 : fullRoute.hashCode()); + result = prime * result + ((host == null) ? 0 : host.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + port; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CFRouteImpl other = (CFRouteImpl) obj; + if (domain == null) { + if (other.domain != null) + return false; + } else if (!domain.equals(other.domain)) + return false; + if (fullRoute == null) { + if (other.fullRoute != null) + return false; + } else if (!fullRoute.equals(other.fullRoute)) + return false; + if (host == null) { + if (other.host != null) + return false; + } else if (!host.equals(other.host)) + return false; + if (path == null) { + if (other.path != null) + return false; + } else if (!path.equals(other.path)) + return false; + if (port != other.port) + return false; + return true; + } + +} diff --git a/vscode-extensions/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTests.java b/vscode-extensions/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTests.java new file mode 100644 index 000000000..fd6677cbf --- /dev/null +++ b/vscode-extensions/commons/commons-cf/src/test/java/org/springframework/ide/vscode/commons/cloudfoundry/client/CFRouteTests.java @@ -0,0 +1,578 @@ +package org.springframework.ide.vscode.commons.cloudfoundry.client; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class CFRouteTests { + + public static final List SPRING_CLOUD_DOMAINS = Arrays.asList("springsource.org", "spring.io", + "myowndomain.spring.io", "tcp.spring.io", "spring.framework"); + + + @Test + public void test_domain_host() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_domain_only() throws Exception { + CFRoute route = CFRoute.builder().from("spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_match() throws Exception { + CFRoute route = CFRoute.builder().from("myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting() throws Exception { + // For domains that do not exist, the first segment is assumed to be the "host" + CFRoute route = CFRoute.builder().from("app.doesnotexist.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("app.doesnotexist.io/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_nonexisting_path_port() throws Exception { + CFRoute route = CFRoute.builder().from("app.doesnotexist.io:60100/withpath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("doesnotexist.io", route.getDomain()); + Assert.assertEquals("app",route.getHost()); + Assert.assertEquals("/withpath",route.getPath()); + Assert.assertEquals(60100, route.getPort()); + Assert.assertEquals("app.doesnotexist.io:60100/withpath", route.getRoute()); + } + + @Test + public void test_longer_domain_match_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.myowndomain.spring.io", route.getRoute()); + } + + @Test + public void test_domain_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath", route.getRoute()); + } + + @Test + public void test_domain_host_path_2() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io/appPath/additionalSegment", SPRING_CLOUD_DOMAINS) + .build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/appPath/additionalSegment", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io/appPath/additionalSegment", route.getRoute()); + } + + @Test + public void test_tcp_port() throws Exception { + CFRoute route = CFRoute.builder().from("tcp.spring.io:9000", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("tcp.spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(9000, route.getPort()); + Assert.assertEquals("tcp.spring.io:9000", route.getRoute()); + } + + @Test + public void test_host_path() throws Exception { + CFRoute route = CFRoute.builder().from("justhost/path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals("justhost/path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_routes() throws Exception { + // A CFRoute does not validate route values. It can create any CF route even with wrong domains + // ports, hosts.. This tests that the route builder is parsing an invalid route into different + // components that some other external mechanism (like the CF Java client) can the use to validate + + CFRoute route = CFRoute.builder().from("", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(null, SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("justhost.", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("justhost",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("justhost.",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from(".justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(".justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("..justdomain", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals(".justdomain",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("..justdomain",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("/justpath/morepath", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/justpath/morepath",route.getPath()); + Assert.assertEquals("/justpath/morepath",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void test_incorrect_ports() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io:notAn1nt3g3r", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp.spring.io:notAn1nt3g3r",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + // Test parsing around the first encountered ':' + route = CFRoute.builder().from("http://myapp.spring.io", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("http://myapp.spring.io",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("tcp.spring.io:8000:9000", SPRING_CLOUD_DOMAINS).build(); + // Only one ':' is allowed. it should not be able to parse a port if more than ':' is encountered + Assert.assertEquals("tcp.spring.io:8000:9000",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + + route = CFRoute.builder().from("myapp.spring.io:8000/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io:8000/",route.getRoute()); + Assert.assertEquals(8000, route.getPort()); + } + + @Test + public void test_incorrect_paths() throws Exception { + CFRoute route = CFRoute.builder().from("myapp.spring.io//path", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("//path",route.getPath()); + Assert.assertEquals("myapp.spring.io//path",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + + route = CFRoute.builder().from("myapp.spring.io/", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/",route.getPath()); + Assert.assertEquals("myapp.spring.io/",route.getRoute()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + } + + @Test + public void parse_null_domain() throws Exception { + String domain = CFRouteBuilder.findDomain("", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(null, SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("...", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain(".cfapps..", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("..cfapps.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void parse_valid_domain() throws Exception { + + // These exist + String domain = CFRouteBuilder.findDomain("spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain(".spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("..spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("spring.io", domain); + + domain = CFRouteBuilder.findDomain("myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + + domain = CFRouteBuilder.findDomain("myapp.myowndomain.spring.io", SPRING_CLOUD_DOMAINS); + Assert.assertEquals("myowndomain.spring.io", domain); + } + + @Test + public void parse_invalid_domain() throws Exception { + + // These variations of existing domains don't exist + String domain = CFRouteBuilder.findDomain("spring.io.", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.cfapps.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("spring.io.cfapps", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + + domain = CFRouteBuilder.findDomain("unknown.domain.io", SPRING_CLOUD_DOMAINS); + Assert.assertNull(domain); + } + + @Test + public void bug_142279275_parse_hostAndPathSameName() throws Exception { + + // Fixes Pivotal Tracker item 142279275 + CFRoute route = CFRoute.builder().from("hello-user.myowndomain.spring.io/hello", SPRING_CLOUD_DOMAINS).build(); + Assert.assertEquals("hello-user", route.getHost()); + Assert.assertEquals("myowndomain.spring.io", route.getDomain()); + Assert.assertEquals("/hello", route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("hello-user.myowndomain.spring.io/hello", route.getRoute()); + } + + @Test + public void build_route_value_empty() throws Exception { + + String val = CFRouteBuilder.buildRouteVal(null, null, null, CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + val = CFRouteBuilder.buildRouteVal("", "", "", CFRoute.NO_PORT); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, val); + + } + + @Test + public void build_route_value() throws Exception { + + String val = CFRouteBuilder.buildRouteVal("appHost", null, null, CFRoute.NO_PORT); + Assert.assertEquals("appHost", val); + + val = CFRouteBuilder.buildRouteVal(null, "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, "/path/to/app", 8000); + Assert.assertEquals(":8000/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal(null, null, null, 60101); + Assert.assertEquals(":60101", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", CFRoute.NO_PORT); + Assert.assertEquals("appHost.cfapps.io/path/to/app", val); + + val = CFRouteBuilder.buildRouteVal("appHost", "cfapps.io", "/path/to/app", 60101); + Assert.assertEquals("appHost.cfapps.io:60101/path/to/app", val); + } + + @Test + public void test_build_route_from_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_nonexisting_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("not.exist.io").build(); + Assert.assertEquals("not.exist.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("not.exist.io", route.getRoute()); + } + + @Test + public void test_build_route_from_null_domain() throws Exception { + CFRoute route = CFRoute.builder().domain(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_domain() throws Exception { + CFRoute route = CFRoute.builder().domain("").build(); + Assert.assertEquals("",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE,route.getRoute()); + } + + @Test + public void test_build_route_from_host() throws Exception { + CFRoute route = CFRoute.builder().host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_null_host() throws Exception { + CFRoute route = CFRoute.builder().host(null).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertNull(route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_empty_host() throws Exception { + CFRoute route = CFRoute.builder().host("").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getPath()); + Assert.assertEquals("",route.getHost()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_domain_host() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp.spring.io", route.getRoute()); + } + + @Test + public void test_build_route_from_path() throws Exception { + CFRoute route = CFRoute.builder().path("/path").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path", route.getRoute()); + } + + @Test + public void test_build_route_from_path_2() throws Exception { + CFRoute route = CFRoute.builder().path("/path/additional").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/additional",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/additional", route.getRoute()); + } + + @Test + public void test_build_route_from_path_3() throws Exception { + CFRoute route = CFRoute.builder().path("/path/").build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/path/",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("/path/", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("spring.io/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_samename() throws Exception { + CFRoute route = CFRoute.builder().path("/myapp").host("myapp").build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/myapp",route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals("myapp/myapp", route.getRoute()); + } + + @Test + public void test_build_route_from_host_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").host("myapp").port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertEquals("myapp",route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_domain_path_port() throws Exception { + CFRoute route = CFRoute.builder().path("/mypath").domain("spring.io").port(8000).build(); + Assert.assertEquals("spring.io", route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertEquals("/mypath",route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("spring.io:8000/mypath", route.getRoute()); + } + + @Test + public void test_build_route_from_port() throws Exception { + CFRoute route = CFRoute.builder().port(8000).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals(":8000", route.getRoute()); + } + + + @Test + public void test_build_route_from_no_port() throws Exception { + CFRoute route = CFRoute.builder().port(CFRoute.NO_PORT).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_build_route_from_no_port_2() throws Exception { + CFRoute route = CFRoute.builder().port(-1).build(); + Assert.assertNull(route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(CFRoute.NO_PORT, route.getPort()); + Assert.assertEquals(CFRoute.EMPTY_ROUTE, route.getRoute()); + } + + @Test + public void test_tcp_port_building() throws Exception { + CFRoute route = CFRoute.builder().domain("tcp.spring.io").port(8000).build(); + Assert.assertEquals("tcp.spring.io",route.getDomain()); + Assert.assertNull(route.getHost()); + Assert.assertNull(route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("tcp.spring.io:8000", route.getRoute()); + } + + + @Test + public void test_complete() throws Exception { + CFRoute route = CFRoute.builder().domain("spring.io").host("myapp").path("/mypath/additional").port(8000).build(); + Assert.assertEquals("spring.io",route.getDomain()); + Assert.assertEquals("myapp", route.getHost()); + Assert.assertEquals("/mypath/additional", route.getPath()); + Assert.assertEquals(8000, route.getPort()); + Assert.assertEquals("myapp.spring.io:8000/mypath/additional", route.getRoute()); + } +} diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java index dec057b68..477c8ad40 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlSchemaProblemsTypes.java @@ -23,5 +23,7 @@ public class ManifestYamlSchemaProblemsTypes { public static final ProblemType UNKNOWN_SERVICES_PROBLEM = problemType("UnknownServicesProblem", ProblemSeverity.WARNING); - + public static final ProblemType UNKNOWN_DOMAIN_PROBLEM = problemType("UnknownDomainProblem", + ProblemSeverity.WARNING); + } diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index 1974ca8c5..d10e05f27 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -99,7 +99,9 @@ public class ManifestYmlSchema implements YamlSchema { // - route: someroute.io YBeanType route = f.ybean("Route"); - route.addProperty(f.yprop("route", t_string).isRequired(true)); + YAtomicType t_route_string = f.yatomic("route"); + route.addProperty(f.yprop("route", t_route_string).isRequired(true)); + t_route_string.parseWith(new RouteValueParser(YTypeFactory.valuesFromHintProvider(domainsProvider))); YAtomicType t_memory = f.yatomic("Memory"); t_memory.addHints("256M", "512M", "1024M"); diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java new file mode 100644 index 000000000..998f44208 --- /dev/null +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/RouteValueParser.java @@ -0,0 +1,52 @@ +package org.springframework.ide.vscode.manifest.yaml; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; + +import org.springframework.ide.vscode.commons.cloudfoundry.client.CFRoute; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException; +import org.springframework.ide.vscode.commons.util.RegexpParser; +import org.springframework.ide.vscode.commons.util.ValueParseException; + +public class RouteValueParser extends RegexpParser { + + private static final String ROUTE_REGEX = "^([\\da-z\\.-]+)(:\\d{1,4})?((\\/[\\dA-Za-z\\.-]+)*\\/?)?$"; + private static final String ROUTE_TYPE_NAME = "Route"; + private static final String ROUTE_DESCRIPTION = "HTTP or TCP application root route"; + + private Callable> domains; + + public RouteValueParser(Callable> domains) { + super(ROUTE_REGEX, ROUTE_TYPE_NAME, ROUTE_DESCRIPTION); + this.domains = domains; + } + + @Override + public Object parse(String str) throws Exception { + Matcher matcher = (Matcher) super.parse(str); + if (matcher != null) { + Collection cloudDomains = domains == null ? Collections.emptyList() : domains.call(); + // Ensure cloud domains is empty list instead of null + if (cloudDomains == null) { + cloudDomains = Collections.emptyList(); + } + CFRoute route = CFRoute.builder().from(str, cloudDomains).build(); + if (route.getDomain() == null || route.getDomain().isEmpty()) { + throw new ValueParseException("Domain is missing."); + } + if ((route.getPath() != null && !route.getPath().isEmpty()) && (route.getPort() != CFRoute.NO_PORT)) { + throw new ValueParseException( + "Unable to determine type of route. HTTP port may have a path but no port. TCP route may have port but no path."); + } + if (!cloudDomains.contains(route.getDomain())) { + String hostDomain = matcher.group(1); + throw new ReconcileException("Unknown domain", ManifestYamlSchemaProblemsTypes.UNKNOWN_DOMAIN_PROBLEM, hostDomain.lastIndexOf(route.getDomain()), hostDomain.length()); + } + return route; + } + return null; + } + +} diff --git a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java index e21a0363e..4e6fe6b15 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java +++ b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java @@ -1013,4 +1013,91 @@ public class ManifestYamlEditorTest { editor.assertContainsCompletions(textAfter); } + @Test + public void reconcileRouteFormat() throws Exception { + Editor editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: http://springsource.org\n"); + editor.assertProblems("http://springsource.org|is not a valid 'Route'"); + + Diagnostic problem = editor.assertProblem("http://springsource.org"); + + assertEquals(DiagnosticSeverity.Error, problem.getSeverity()); + + editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: spring source.org\n"); + editor.assertProblems("spring source.org|is not a valid 'Route'"); + + problem = editor.assertProblem("spring source.org"); + + assertEquals(DiagnosticSeverity.Error, problem.getSeverity()); + + editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: springsource.org:kuku\n"); + editor.assertProblems("springsource.org:kuku|is not a valid 'Route'"); + + problem = editor.assertProblem("springsource.org:kuku"); + + assertEquals(DiagnosticSeverity.Error, problem.getSeverity()); + + + editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: springsource.org/kuku?p=23\n"); + editor.assertProblems("springsource.org/kuku?p=23|is not a valid 'Route'"); + + problem = editor.assertProblem("springsource.org/kuku?p=23"); + + assertEquals(DiagnosticSeverity.Error, problem.getSeverity()); + } + + @Test + public void reconcileRoute_Advanced() throws Exception { + Editor editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: springsource.org:8765/path\n"); + editor.assertProblems("springsource.org:8765/path|Unable to determine type of route"); + + Diagnostic problem = editor.assertProblem("springsource.org:8765/path"); + + assertEquals(DiagnosticSeverity.Error, problem.getSeverity()); + + editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: host.springsource.org\n"); + editor.assertProblems("springsource.org|Unknown domain"); + + problem = editor.assertProblem("springsource.org"); + + assertEquals(DiagnosticSeverity.Warning, problem.getSeverity()); + } + + @Test + public void reconcileRouteValidDomain() throws Exception { + ClientRequests cfClient = cloudfoundry.client; + CFDomain domain = Mockito.mock(CFDomain.class); + when(domain.getName()).thenReturn("springsource.org"); + when(cfClient.getDomains()).thenReturn(ImmutableList.of(domain)); + Editor editor = harness.newEditor( + "applications:\n" + + "- name: foo\n" + + " routes:\n" + + " - route: host.springsource.org\n"); + editor.assertProblems(); + } + }