/******************************************************************************* * Copyright (c) 2017 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import org.cloudfoundry.operations.routes.Route; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCloudDomain; import org.springframework.ide.eclipse.boot.util.Log; import org.springframework.ide.eclipse.editor.support.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 CFRoute(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<String> 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<String> 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<CFCloudDomain> cloudDomains) { List<String> domains = cloudDomains .stream() .map(CFCloudDomain::getName) .collect(Collectors.toList()); return from(desiredUrl, domains); } }