/*******************************************************************************
* Copyright (c) 2016, 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.deployment;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ApplicationManifestHandler;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCloudDomain;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2.CFRoute;
import org.springframework.ide.eclipse.boot.util.Log;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.DumperOptions.LineBreak;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.composer.Composer;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.parser.ParserImpl;
import org.yaml.snakeyaml.reader.StreamReader;
import org.yaml.snakeyaml.resolver.Resolver;
import com.google.common.base.Objects;
/**
* Deployment properties based on YAML Graph. Instance of this class has ability
* to compute text differences between this instance and deployment properties
* passed as parameter
*
* @author Alex Boyko
*
*/
public class YamlGraphDeploymentProperties implements DeploymentProperties {
private String content;
private MappingNode appNode;
private Node root;
private SequenceNode applicationsValueNode;
private Yaml yaml;
private Map<String, Object> cloudData;
public YamlGraphDeploymentProperties(String content, String appName, Map<String, Object> cloudData) {
super();
this.appNode = null;
this.applicationsValueNode = null;
this.root = null;
this.cloudData = cloudData;
this.content = content;
initializeYaml(appName);
}
private void initializeYaml(String appName) {
Composer composer = new Composer(new ParserImpl(new StreamReader(new InputStreamReader(new ByteArrayInputStream(content.getBytes())))), new Resolver());
root = composer.getSingleNode();
Node apps = YamlGraphDeploymentProperties.findValueNode(root, "applications");
if (apps instanceof SequenceNode) {
applicationsValueNode = (SequenceNode) apps;
appNode = findAppNode(applicationsValueNode, appName);
} else if (root instanceof MappingNode) {
appNode = (MappingNode) root;
}
this.yaml = new Yaml(createDumperOptions());
}
private static MappingNode findAppNode(SequenceNode seq, String name) {
if (name != null) {
for (Node n : seq.getValue()) {
Node nameValue = findValueNode(n, ApplicationManifestHandler.NAME_PROP);
if (nameValue instanceof ScalarNode && ((ScalarNode)nameValue).getValue().equals(name)) {
return (MappingNode) n;
}
}
}
return null;
}
public static DumperOptions createDumperOptions() {
DumperOptions options = new DumperOptions();
options.setExplicitStart(false);
options.setCanonical(false);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(FlowStyle.BLOCK);
options.setLineBreak(LineBreak.getPlatformLineBreak());
return options;
}
@SuppressWarnings("unchecked")
static public <T extends Node> T getNode(Node node, String key, Class<T> type) {
Node n = findValueNode(node, key);
if (n != null && type.isAssignableFrom(n.getClass())) {
return (T) n;
}
return null;
}
@Override
public String getAppName() {
/*
* Name must be located in the app node!
*/
return getPropertyValue(appNode, ApplicationManifestHandler.NAME_PROP, String.class);
}
@Override
public int getMemory() {
String memoryStringValue = getAbsoluteValue(ApplicationManifestHandler.MEMORY_PROP, String.class);
if (memoryStringValue != null) {
try {
return ApplicationManifestHandler.convertMemory(memoryStringValue);
} catch (CoreException e) {
Log.log(e);
}
}
return DeploymentProperties.DEFAULT_MEMORY;
}
public String getInheritFilePath() {
return getPropertyValue(root, ApplicationManifestHandler.INHERIT_PROP, String.class);
}
@Override
public String getBuildpack() {
return getAbsoluteValue(ApplicationManifestHandler.BUILDPACK_PROP, String.class);
}
@SuppressWarnings("unchecked")
@Override
public Map<String, String> getEnvironmentVariables() {
Map<String, String> map = getAbsoluteValue(ApplicationManifestHandler.ENV_PROP, Map.class);
return map == null ? Collections.<String, String>emptyMap() : map;
}
@Override
public int getInstances() {
Integer n = getAbsoluteValue(ApplicationManifestHandler.INSTANCES_PROP, Integer.class);
return n == null ? DeploymentProperties.DEFAULT_INSTANCES : n.intValue();
}
@Override
public Integer getTimeout() {
return getAbsoluteValue(ApplicationManifestHandler.TIMEOUT_PROP, Integer.class);
}
@Override
public String getCommand() {
return getAbsoluteValue(ApplicationManifestHandler.COMMAND_PROP, String.class);
}
@Override
public String getHealthCheckType() {
return getAbsoluteValue(ApplicationManifestHandler.HEALTH_CHECK_TYPE_PROP, String.class);
}
@Override
public String getStack() {
return getAbsoluteValue(ApplicationManifestHandler.STACK_PROP, String.class);
}
@SuppressWarnings("unchecked")
@Override
public List<String> getServices() {
List<String> services = getAbsoluteValue(ApplicationManifestHandler.SERVICES_PROP, List.class);
return services == null ? Collections.<String>emptyList() : services;
}
public static Node findValueNode(Node node, String key) {
if (node instanceof MappingNode) {
MappingNode mapping = (MappingNode) node;
for (NodeTuple tuple : mapping.getValue()) {
if (tuple.getKeyNode() instanceof ScalarNode) {
ScalarNode scalar = (ScalarNode) tuple.getKeyNode();
if (key.equals(scalar.getValue())) {
return tuple.getValueNode();
}
}
}
}
return null;
}
public static NodeTuple findNodeTuple(MappingNode mapping, String key) {
if (mapping != null) {
for (NodeTuple tuple : mapping.getValue()) {
if (tuple.getKeyNode() instanceof ScalarNode) {
ScalarNode scalar = (ScalarNode) tuple.getKeyNode();
if (key.equals(scalar.getValue())) {
return tuple;
}
}
}
}
return null;
}
private ReplaceEdit addLineBreakIfMissing(int index) {
int i = index - 1;
for (; i >= 0 && Character.isWhitespace(content.charAt(i)) && content.charAt(i) != '\n'; i--);
if (i > 0 && content.charAt(i) != '\n') {
return new ReplaceEdit(index, 0, System.lineSeparator());
}
return null;
}
public MultiTextEdit getDifferences(DeploymentProperties props) {
MultiTextEdit edits = new MultiTextEdit();
TextEdit edit;
if (appNode == null) {
Map<Object, Object> obj = ApplicationManifestHandler.toYaml(props, cloudData, isLegacyHostDomainManifestYaml(root));
if (applicationsValueNode == null) {
DumperOptions options = new DumperOptions();
options.setExplicitStart(true);
options.setCanonical(false);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(FlowStyle.BLOCK);
options.setLineBreak(LineBreak.getPlatformLineBreak());
edits.addChild(new ReplaceEdit(0, content.length(), new Yaml(options).dump(obj)));
} else {
edit = addLineBreakIfMissing(applicationsValueNode.getEndMark().getIndex());
if (edit != null) {
edits.addChild(edit);
}
@SuppressWarnings("unchecked")
/*
* Find the appropriate application Object in the list.
*/
List<Object> appsObj = (List<Object>) obj.get(ApplicationManifestHandler.APPLICATIONS_PROP);
Object appObject = appsObj.get(0);
for (Object entry : appsObj) {
if (entry instanceof Map<?,?> && Objects.equal(props.getAppName(), ((Map<?,?>)entry).get(ApplicationManifestHandler.NAME_PROP))) {
appObject = entry;
break;
}
}
edits.addChild(new ReplaceEdit(applicationsValueNode.getEndMark().getIndex(), 0, serializeListEntry(appObject, applicationsValueNode.getStartMark().getColumn()).toString()));
}
} else {
if (!Objects.equal(getAppName(), props.getAppName())) {
edit = createEdit(appNode, props.getAppName(), ApplicationManifestHandler.NAME_PROP);
if (edit != null) {
edits.addChild(edit);
}
}
/*
* Compare value because strings may have 'G', 'M' etc post-fixes
*/
if (getMemory() != props.getMemory()) {
edit = createEdit(appNode, String.valueOf(props.getMemory()) + "M", ApplicationManifestHandler.MEMORY_PROP);
if (edit != null) {
edits.addChild(edit);
}
}
if (getInstances() != props.getInstances()) {
getDifferenceForEntry(edits, ApplicationManifestHandler.INSTANCES_PROP, props.getInstances(), DEFAULT_INSTANCES, Integer.class);
}
if (!Objects.equal(getTimeout(), props.getTimeout())) {
getDifferenceForEntry(edits, ApplicationManifestHandler.TIMEOUT_PROP, props.getTimeout(), null, Integer.class);
}
if (!Objects.equal(getHealthCheckType(), props.getHealthCheckType())) {
getDifferenceForEntry(edits, ApplicationManifestHandler.HEALTH_CHECK_TYPE_PROP, props.getHealthCheckType(),
DeploymentProperties.DEFAULT_HEALTH_CHECK_TYPE, String.class);
}
if (!Objects.equal(getCommand(), props.getCommand())) {
getDifferenceForEntry(edits, ApplicationManifestHandler.COMMAND_PROP, props.getCommand(), null, String.class);
}
/*
* Only if 'stack' attribute is present in the manifest perform the comparison
*/
if (getStack() != null && !getStack().equals(props.getStack())) {
getDifferenceForEntry(edits, ApplicationManifestHandler.STACK_PROP, props.getStack(), null, String.class);
}
if (getDiskQuota() != props.getDiskQuota()) {
edit = createEdit(appNode, String.valueOf(props.getDiskQuota()) + "M", ApplicationManifestHandler.DISK_QUOTA_PROP);
if (edit != null) {
edits.addChild(edit);
}
}
if (!Objects.equal(getBuildpack(), props.getBuildpack())) {
edit = createEdit(appNode, props.getBuildpack(), ApplicationManifestHandler.BUILDPACK_PROP);
if (edit != null) {
edits.addChild(edit);
}
}
if (!new HashSet<>(getServices()).equals(new HashSet<>(props.getServices()))) {
getDifferencesForList(edits, ApplicationManifestHandler.SERVICES_PROP, props.getServices(), String.class);
}
if (!getEnvironmentVariables().equals(props.getEnvironmentVariables())) {
getDifferencesForMap(edits, ApplicationManifestHandler.ENV_PROP, props.getEnvironmentVariables());
}
/*
* If any text edits are produced then there are differences in the URIs
*/
Set<String> currentUris = getUris();
Set<String> otherUris = props.getUris();
if (!isRandomRouteMatch(currentUris, otherUris) && !currentUris.equals(otherUris)) {
if (isLegacyHostDomainManifestYaml(root)) {
getLegacyDifferenceForUris(otherUris, edits);
} else {
getDifferenceForUris(otherUris, edits);
}
}
}
return edits.hasChildren() ? edits : null;
}
private boolean isRandomRouteMatch(Set<String> currentUris, Set<String> otherUris) {
if (currentUris.size() == 1 && otherUris.size() == 1) {
String uri = currentUris.iterator().next();
String host = uri.substring(0, uri.indexOf('.'));
return ApplicationManifestHandler.RANDOM_VAR.equals(host);
}
return false;
}
/**
* Creates diff text edits for entry in the map node given by the
* attribute's name based on its new value and type as well as the default
* value that is not serialized under normal circumstances
*
* @param me
* container to append text edits
* @param key
* manifest attribute name
* @param newValue
* the new value to create diff edits for
* @param defaultValue
* the default value for the attribute that is usually not
* serialized
* @param type
* type of the value for the attribute
*/
private <T> void getDifferenceForEntry(MultiTextEdit me, String key, T newValue, T defaultValue, Class<T> type) {
TextEdit edit = null;
if (Objects.equal(newValue, defaultValue)) {
/*
* New value is the default value. Check if entry can be safely removed from YAML
*/
T rootValue = getPropertyValue(root, key, type);
if (appNode != root && rootValue != null) {
if (newValue == null) {
me.addChild(createEdit((MappingNode)root, (Object) null, key));
for (Node n : applicationsValueNode.getValue()) {
if (n instanceof MappingNode) {
MappingNode application = (MappingNode) n;
if (application == appNode) {
edit = createEdit(appNode, (Object) null, key);
if (edit != null) {
me.addChild(edit);
}
} else {
T appValue = getPropertyValue(application, key, type);
if (appValue == null) {
me.addChild(createEdit(application, rootValue, key));
}
}
}
}
} else {
edit = createEdit(appNode, newValue, key);
if (edit != null) {
me.addChild(edit);
}
}
} else {
edit = createEdit(appNode, (T) null, key);
if (edit != null) {
me.addChild(edit);
}
}
} else {
/*
* New value is not default hence it have to be serialized and
* therefore would override the value in the root node for the same
* attribute
*/
edit = createEdit(appNode, newValue, key);
if (edit != null) {
me.addChild(edit);
}
}
}
/**
* Creates text edits based on differences between current manifest
* attribute list value and the passed new list value. The manifest
* attribute value is considered to be defined either on the application or
* the root node of the manifest YAML and text edit is calculated
* accordingly. It also supports multiple apps defined in the manifest
*
* @param me
* multi text edit gathering all edits
* @param key
* manifest attribute name
* @param newValue
* the new value to set
*/
@SuppressWarnings("unchecked")
private <T> void getDifferencesForList(MultiTextEdit me, String key, List<T> newValue, Class<T> type) {
TextEdit edit;
/*
* Moved new value entries in the set to avoid duplication
*/
LinkedHashSet<T> otherValue = new LinkedHashSet<>(newValue);
/*
* Get the list value from the root node
*/
List<T> rootList = root != appNode ? getPropertyValue(root, key, List.class) : Collections.emptyList();
if (rootList == null) {
rootList = Collections.emptyList();
}
if (otherValue.containsAll(rootList)) {
/*
* All list entries from the root are present in the new value
*/
otherValue.removeAll(rootList);
/*
* Create an edit for a difference between the remaining list of
* values and current app's node list
*/
edit = createEdit(appNode, new ArrayList<>(otherValue), key, type);
if (edit != null) {
me.addChild(edit);
}
} else {
/*
* Some list entries from the root are missing move all root values
* to application nodes. Applications value node must be present
* because rootList wasn't empty since we got here
*/
for (Node n : applicationsValueNode.getValue()) {
if (n instanceof MappingNode) {
MappingNode application = (MappingNode) n;
if (n == appNode) {
/*
* Current app node
*/
edit = createEdit(application, newValue, key, type);
if (edit != null) {
me.addChild(edit);
}
} else {
/*
* Any other app node. Get its list value for the attribute
*/
List<T> currentValues = getPropertyValue(application, key, List.class);
if (currentValues == null) {
/*
* There is no value for the property so just create an edit for the list from the root
*/
edit = createEdit(application, rootList, key, type);
} else {
/*
* Create a joint list of values from app's node list value and root node list value
*/
LinkedHashSet<T> values = new LinkedHashSet<>(currentValues);
values.addAll(rootList);
/*
* Create and edit with the new value being the joint list
*/
edit = createEdit(application, new ArrayList<>(values), key, type);
}
if (edit != null) {
me.addChild(edit);
}
}
}
}
/*
* Remove the list from the root node
*/
edit = createEdit((MappingNode)root, (Object)null, key);
if (edit != null) {
me.addChild(edit);
}
}
}
/**
* Creates text edits based on differences between current manifest
* attribute map value and the passed new map value. The manifest
* attribute value is considered to be defined either on the application or
* the root node of the manifest YAML and text edit is calculated
* accordingly. It also supports multiple apps defined in the manifest
*
* @param me
* multi text edit gathering all edits
* @param key
* manifest attribute name
* @param newValue
* the new value to set
*/
@SuppressWarnings("unchecked")
private void getDifferencesForMap(MultiTextEdit me, String key, Map<String, String> newValue) {
TextEdit edit;
/*
* Get the map value from the root node
*/
Map<String, String> rootMap = root != appNode ? getPropertyValue(root, key, Map.class) : Collections.emptyMap();
/*
* Copy the new value to leave it unchanged
*/
LinkedHashMap<String, String> otherValue = new LinkedHashMap<>(newValue);
if (rootMap == null) {
rootMap = Collections.emptyMap();
}
if (otherValue.keySet().containsAll(rootMap.keySet())) {
/*
* All map entries from the root are present in the new value
*/
for (String k : rootMap.keySet()) {
if (Objects.equal(otherValue.get(k), rootMap.get(k))) {
otherValue.remove(k);
}
}
/*
* Create an edit for a difference between the remaining map and
* current app's node map
*/
edit = createEdit(appNode, otherValue, key);
if (edit != null) {
me.addChild(edit);
}
} else {
/*
* Some map entries from the root must be removed. Move root node map to applications.
* Applications value node must be present because rootList wasn't empty since we got here.
*/
for (Node n : applicationsValueNode.getValue()) {
if (n instanceof MappingNode) {
MappingNode application = (MappingNode) n;
if (n == appNode) {
/*
* Current app node
*/
edit = createEdit(application, newValue, key);
if (edit != null) {
me.addChild(edit);
}
} else {
/*
* Any other app node. Get its map value for the attribute
*/
Map<String, String> currentValues = getPropertyValue(application, key, Map.class);
if (currentValues == null) {
/*
* There is no value for the property so just create an edit for the map from the root
*/
edit = createEdit(application, rootMap, key);
} else {
/*
* Create a joint map of entries from app's node map value and root node map value
*/
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
if (!currentValues.containsKey(entry.getKey())) {
currentValues.put(entry.getKey(), entry.getValue());
}
}
/*
* Create and edit with the new value being the joint map
*/
edit = createEdit(application, currentValues, key);
}
if (edit != null) {
me.addChild(edit);
}
}
}
}
/*
* Remove the list from the root node
*/
edit = createEdit((MappingNode)root, Collections.<String, String>emptyMap(), key);
if (edit != null) {
me.addChild(edit);
}
}
}
private void getDifferenceForUris(Collection<String> uris, MultiTextEdit me) {
Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class);
Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class);
boolean otherNoRoute = uris.isEmpty();
boolean match = false;
if (otherNoRoute) {
if (!Boolean.TRUE.equals(noRoute)) {
getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, true, false, Boolean.class);
if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP));
}
} else {
match = true;
}
} else {
if (Boolean.TRUE.equals(noRoute)) {
getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, false, false, Boolean.class);
}
if (Boolean.TRUE.equals(randomRoute) && uris.size() == 1 && getAbsoluteValue(ApplicationManifestHandler.ROUTES_PROP, Map.class) == null) {
match = true;
} else if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP));
}
}
if (!match) {
getDifferencesForList(me, ApplicationManifestHandler.ROUTES_PROP, uris.stream().map(uri -> {
Map<Object, Object> routeObj = new LinkedHashMap<>();
routeObj.put(ApplicationManifestHandler.ROUTE_PROP, uri);
return routeObj;
}).collect(Collectors.toList()), Map.class);
}
}
private void getLegacyDifferenceForUris(Collection<String> uris, MultiTextEdit me) {
List<CFCloudDomain> domains = ApplicationManifestHandler.getCloudDomains(cloudData);
LinkedHashSet<String> otherHosts = new LinkedHashSet<>();
LinkedHashSet<String> otherDomains = new LinkedHashSet<>();
ApplicationManifestHandler.extractHostsAndDomains(uris, domains, otherHosts, otherDomains);
boolean otherNoRoute = otherHosts.isEmpty() && otherDomains.isEmpty();
boolean otherNoHostname = otherHosts.isEmpty() && !otherDomains.isEmpty();
LinkedHashSet<String> currentHosts = new LinkedHashSet<>();
LinkedHashSet<String> currentDomains = new LinkedHashSet<>();
/*
* Gather hosts from "host" and "hosts" attributes from app and root nodes
*/
String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class);
if (host != null) {
currentHosts.add(host);
}
List<?> hostsList = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAINS_PROP, List.class);
if (hostsList != null) {
for (Object o : hostsList) {
if (o instanceof String) {
currentHosts.add((String) o);
}
}
}
/*
* Gather domains from 'domain' and 'domains' attributes from app and root nodes
*/
String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class);
if (domain != null) {
currentDomains.add(domain);
}
List<?> domainsList = getAbsoluteValue(ApplicationManifestHandler.DOMAINS_PROP, List.class);
if (domainsList != null) {
for (Object o : domainsList) {
if (o instanceof String) {
currentDomains.add((String) o);
}
}
}
boolean match = false;
Boolean noHost = getAbsoluteValue(ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class);
Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class);
Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class);
if (otherNoRoute) {
if (!Boolean.TRUE.equals(noRoute)) {
getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, true, false, Boolean.class);
if (getPropertyValue(appNode, ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.NO_HOSTNAME_PROP));
}
if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP));
}
} else {
match = true;
}
} else {
if (Boolean.TRUE.equals(noRoute)) {
getDifferenceForEntry(me, ApplicationManifestHandler.NO_ROUTE_PROP, false, false, Boolean.class);
}
if (otherNoHostname) {
if (!Boolean.TRUE.equals(noHost)) {
me.addChild(createEdit(appNode, Boolean.TRUE, ApplicationManifestHandler.NO_HOSTNAME_PROP));
}
} else {
/*
* There is at least a host in the deployment properties. Remove
* "no-hostname" attribute if there is one from the application
* node. Don't care if it's in the root or anywhere else
*/
if (getPropertyValue(appNode, ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.NO_HOSTNAME_PROP));
}
}
if (Boolean.TRUE.equals(randomRoute) && otherHosts.size() == 1 && otherDomains.size() == 1 && currentHosts.isEmpty()) {
match = true;
} else if (getPropertyValue(appNode, ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class) != null) {
me.addChild(createEdit(appNode, (String) null, ApplicationManifestHandler.RANDOM_ROUTE_PROP));
}
if (currentHosts.isEmpty() && !Boolean.TRUE.equals(noHost)) {
currentHosts.add(getAppName());
}
if (currentDomains.isEmpty() && !domains.isEmpty()) {
currentDomains.add(domains.get(0).getName());
}
}
if (!match && (!currentHosts.equals(otherHosts) || !currentDomains.equals(otherDomains))) {
generateEditForHostsAndDomains(me, currentHosts, currentDomains, otherHosts, otherDomains);
}
}
private void generateEditForHostsAndDomains(MultiTextEdit me, Set<String> currentHosts, Set<String> currentDomains, Set<String> otherHosts, Set<String> otherDomains) {
/*
* Calculate current 'host' attrbute value
*/
String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class);
if (otherHosts.size() == 1) {
/*
* Only one host for deployment props
*/
String otherHost = otherHosts.iterator().next();
/*
* If calculated host is different from deployment props create edit
*/
if (host == null || !otherHost.equals(host)) {
getDifferenceForEntry(me, ApplicationManifestHandler.SUB_DOMAIN_PROP, otherHost, null, String.class);
}
/*
* Ensure the deployment props hosts are empty since the difference has been dealt with here
*/
otherHosts.clear();
} else {
/*
* Deployment props have more than one host.
* Check if current "host" attribute value is one of the hosts from deployment props
*/
if (host != null && !otherHosts.remove(host)) {
/*
* If current 'host' attribute value is not contained in
* deployment props hosts then ensure "host" attribute value is
* cleared
*/
getDifferenceForEntry(me, ApplicationManifestHandler.SUB_DOMAIN_PROP, null, null, String.class);
}
}
/*
* Calculate edit for hosts list
*/
getDifferencesForList(me, ApplicationManifestHandler.SUB_DOMAINS_PROP, new ArrayList<>(otherHosts), String.class);
/*
* Calculate current 'domain' attribute value
*/
String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class);
if (otherDomains.size() == 1) {
/*
* Only one domain for deployment props
*/
String otherDomain = otherDomains.iterator().next();
/*
* If calculated domain is different from deployment props create edit
*/
if (domain == null || !otherDomain.equals(domain)) {
getDifferenceForEntry(me, ApplicationManifestHandler.DOMAIN_PROP, otherDomain, null, String.class);
}
/*
* Ensure the deployment props domains are empty since the difference has been dealt with here
*/
otherDomains.clear();
} else {
/*
* Deployment props have more than one domain.
* Check if current "domain" attribute value is one of the domains from deployment props
*/
if (domain != null && !otherDomains.remove(domain)) {
/*
* If current 'domain' attribute value is not contained in
* deployment props domains then ensure "domain" attribute value is
* cleared
*/
getDifferenceForEntry(me, ApplicationManifestHandler.DOMAIN_PROP, null, null, String.class);
}
}
/*
* Calculate edit for domains list
*/
getDifferencesForList(me, ApplicationManifestHandler.DOMAINS_PROP, new ArrayList<>(otherDomains), String.class);
}
/**
* Creates text edit for mapping node tuples where property and value are
* scalars (i.e. value is either string or some primitive type)
*
* @param parent
* the parent MappingNode
* @param otherValue
* the new value for the tuple
* @param property
* tuple's key
* @return the text edit
*/
private TextEdit createEdit(MappingNode parent, Object otherValue, String property) {
NodeTuple tuple = findNodeTuple(parent, property);
if (tuple == null) {
if (otherValue != null) {
StringBuilder serializedValue = serialize(property, otherValue);
boolean[] postIndent = new boolean[] { true };
int position = positionToAppendAt(parent, postIndent);
if (postIndent[0]) {
postIndent(serializedValue, getDefaultOffset());
} else {
preIndent(serializedValue, getDefaultOffset());
}
return new ReplaceEdit(position, 0, serializedValue.toString());
}
} else {
if (otherValue == null) {
/*
* Delete the tuple including the line break if possible
*/
int start = tuple.getKeyNode().getStartMark().getIndex();
int end = tuple.getValueNode().getEndMark().getIndex();
/*
* k1: v1
* k-delete: v-delete
* ^ ^
* start index end index
* k2: v2
*
* Extend end index to position of k2 and leave start index where it was with correct indent
*
* However, if it' the last tuple in the map than just delete it and leave the line with the indent in the beginning for now.
*/
if (parent.getValue().get(parent.getValue().size() - 1) != tuple) {
for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)); end++);
}
return new DeleteEdit(start, end - start);
} else {
/*
* Replace the current value (whether it's a scalr value or anything else without affecting the white space
*/
return new ReplaceEdit(tuple.getValueNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - tuple.getValueNode().getStartMark().getIndex(), String.valueOf(otherValue));
// return createReplaceEditWithoutWhiteSpace(tuple.getValueNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - 1,
// String.valueOf(otherValue));
}
}
return null;
}
/**
* Calculates position to append entries to the map node. Also provides a
* hint on how to properly append entries. If entries are to be appended
* after the last entry in the map node then they need to be all
* pre-indented, and post-indented otherwise
*
* @param m the map node
* @param postIndent the post- or pre- indent calculated hint
* @return the index to append entries
*/
private int positionToAppendAt(MappingNode m, boolean[] postIndent) {
/*
* Check if there is a name attribute in the map node (case of application node) and make the end index of tha 'name: XXX' tuple as the index to append
*/
for (NodeTuple tuple : m.getValue()) {
if (tuple.getKeyNode() instanceof ScalarNode
&& ApplicationManifestHandler.NAME_PROP.equals(((ScalarNode) tuple.getKeyNode()).getValue())) {
int index = tuple.getValueNode().getEndMark().getIndex();
for (; index > 0 && index < content.length() && Character.isWhitespace(content.charAt(index)); index++)
;
postIndent[0] = m.getValue().get(m.getValue().size() - 1) != tuple;
return index;
}
}
postIndent[0] = true;
return m.getStartMark().getIndex();
}
private <T> TextEdit createEdit(MappingNode parent, List<T> otherValue, String property, Class<T> type) {
NodeTuple tuple = findNodeTuple(parent, property);
if (tuple == null) {
if (otherValue != null && !otherValue.isEmpty()) {
StringBuilder serializedValue = serialize(property, otherValue);
// postIndent(serializedValue, getDefaultOffset());
// int position = positionToAppendAt(parent);
boolean[] postIndent = new boolean[] { true };
int position = positionToAppendAt(parent, postIndent);
if (postIndent[0]) {
postIndent(serializedValue, getDefaultOffset());
} else {
preIndent(serializedValue, getDefaultOffset());
}
return new ReplaceEdit(position, 0, serializedValue.toString());
}
} else {
if (otherValue == null || otherValue.isEmpty()) {
int start = tuple.getKeyNode().getStartMark().getIndex();
int end = tuple.getValueNode().getEndMark().getIndex();
// int index = parent.getValue().indexOf(tuple);
// if (!(index > 0 && parent.getValue().get(index - 1).getValueNode() instanceof CollectionNode)) {
// /*
// * If previous tuple is not a map or list then try to remove the preceding line break
// */
// for (; start > 0 && Character.isWhitespace(content.charAt(start - 1)) && content.charAt(start - 1) != '\n'; start--);
// }
for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)); end++);
return new DeleteEdit(start, end - start);
} else {
Node sequence = tuple.getKeyNode();
if (tuple.getValueNode() instanceof SequenceNode) {
SequenceNode sequenceValue = (SequenceNode) tuple.getValueNode();
MultiTextEdit me = new MultiTextEdit();
Set<T> others = new LinkedHashSet<>();
others.addAll(otherValue);
/*
* Remember the ending position of the last entry that remains in the list
*/
int appendIndex = sequenceValue.getEndMark().getIndex();
for (int index = 0; index < sequenceValue.getValue().size(); index++) {
Node n = sequenceValue.getValue().get(index);
T value = getValue(n, type);
if (others.contains(value)) {
// Entry exists, do nothing, just update the end position to append the missing entries
others.remove(value);
appendIndex = n.getEndMark().getIndex();
} else {
/*
* skip "- " prefix for the start position
*/
int start = n.getStartMark().getIndex();
for (; start > 0 && content.charAt(start) != '-' && content.charAt(start) != '\n'; start--);
int end = n.getEndMark().getIndex();
// If entry is object in the list don't remove the indent for the next entry in YAML (if there is a next YAML entry)
/*
* - e: entry-1
* ^-start
* - e: entry-2
* ^-end
*/
if (n instanceof MappingNode && root.getEndMark().getIndex() != end) {
if (parent.getEndMark().getIndex() == n.getEndMark().getIndex()) {
// last entry in the list but not the last yaml piece in the document
end -= (parent.getStartMark().getColumn() - appNode.getStartMark().getColumn());
} else {
end -= sequenceValue.getStartMark().getColumn();
}
}
/*
* "- entry" start=2, end=7, need to include '\n' in the deletion
*/
DeleteEdit deleteEdit = createDeleteEditIncludingLine(start, end);
appendIndex = deleteEdit.getOffset();
me.addChild(deleteEdit);
}
}
/*
* TODO: verify that further appendIndex manipulations are necessary!
*/
/*
* Offset appendIndex to leave the line break for the previous entry in place. jump over spacing and line break.
*/
for (; appendIndex > 0 && appendIndex < content.length() && Character.isWhitespace(content.charAt(appendIndex)) && content.charAt(appendIndex - 1) != '\n'; appendIndex++);
/*
* Add a line break if append index is not starting right after line break.
*/
if (!others.isEmpty() && content.charAt(appendIndex - 1) != '\n') {
me.addChild(new ReplaceEdit(appendIndex, 0, System.lineSeparator()));
}
/*
* Add missing entries
*/
for (T s : others) {
me.addChild(new ReplaceEdit(appendIndex, 0, serializeListEntry(s, sequenceValue.getStartMark().getColumn()).toString()));
}
return me.hasChildren() ? me : null;
} else {
/*
* Sequence is expected but was something else. Replace the
* whole tuple. Don't touch the whitespace when replacing -
* it looks good
*/
StringBuilder s = serialize(property, otherValue);
preIndent(s, sequence.getStartMark().getColumn());
return createReplaceEditWithoutWhiteSpace(sequence.getStartMark().getIndex(),
tuple.getValueNode().getEndMark().getIndex() - 1,
s.toString().trim());
}
}
}
return null;
}
private TextEdit createEdit(MappingNode parent, Map<String, String> otherValue, String property) {
NodeTuple tuple = findNodeTuple(parent, property);
if (tuple == null) {
/*
* No tuple found for the key
*/
if (otherValue != null && !otherValue.isEmpty()) {
/*
* If other value is something that can be serialized, serialize the key and other value and put in the YAML
*/
StringBuilder serializedValue = serialize(property, otherValue);
// postIndent(serializedValue, getDefaultOffset());
// int position = positionToAppendAt(parent);
boolean[] postIndent = new boolean[] { true };
int position = positionToAppendAt(parent, postIndent);
if (postIndent[0]) {
postIndent(serializedValue, getDefaultOffset());
} else {
preIndent(serializedValue, getDefaultOffset());
}
return new ReplaceEdit(position, 0, serializedValue.toString());
}
} else {
/*
* Tuple with the string key is found
*/
if (otherValue == null || otherValue.isEmpty()) {
/*
* Delete the found tuple since other value is null or empty
*/
int start = tuple.getKeyNode().getStartMark().getIndex();
int end = tuple.getValueNode().getEndMark().getIndex();
return new DeleteEdit(start, end - start);
// return createDeleteEditIncludingLine(tuple.getKeyNode().getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex());
} else {
/*
* Tuple is found, so the key node is there, check the value node
*/
Node map = tuple.getKeyNode();
if (tuple.getValueNode() instanceof MappingNode) {
/*
* Value node is a map node. Go over every entry in the map to calculate differences
*/
MappingNode mapValue = (MappingNode) tuple.getValueNode();
MultiTextEdit e = new MultiTextEdit();
Map<String, String> leftOver = new LinkedHashMap<>();
leftOver.putAll(otherValue);
int appendIndex = mapValue.getStartMark().getIndex();
for (NodeTuple t : mapValue.getValue()) {
if (t.getKeyNode() instanceof ScalarNode && t.getValueNode() instanceof ScalarNode) {
ScalarNode key = (ScalarNode) t.getKeyNode();
ScalarNode value = (ScalarNode) t.getValueNode();
String newValue = leftOver.get(key.getValue());
if (newValue == null) {
/*
* Delete the tuple if newValue is null. Delete including the line if necessary
*/
e.addChild(createDeleteEditIncludingLine(key.getStartMark().getIndex(), value.getEndMark().getIndex()));
} else if (!value.getValue().equals(newValue)) {
/*
* Key is there but value is different, so edit the value
*/
e.addChild(new ReplaceEdit(value.getStartMark().getIndex(), value.getEndMark().getIndex() - value.getStartMark().getIndex(), newValue));
appendIndex = value.getEndMark().getIndex();
} else {
appendIndex = value.getEndMark().getIndex();
}
leftOver.remove(key.getValue());
}
}
/*
* Offset appendIndex to leave the line break for the previous entry in place. jump over spacing and line break.
*/
for (; appendIndex > 0 && appendIndex < content.length() && Character.isWhitespace(content.charAt(appendIndex)) && content.charAt(appendIndex - 1) != '\n'; appendIndex++);
/*
* Add a line break if append index is not starting right after line break.
*/
if (!leftOver.isEmpty() && content.charAt(appendIndex - 1) != '\n') {
e.addChild(new ReplaceEdit(appendIndex, 0, System.lineSeparator()));
}
/*
* Add remaining unmatched entries
*/
for (Map.Entry<String, String> entry : leftOver.entrySet()) {
StringBuilder serializedValue = serialize(entry.getKey(), entry.getValue());
preIndent(serializedValue, mapValue.getStartMark().getColumn());
e.addChild(new ReplaceEdit(appendIndex, 0, serializedValue.toString()));
}
return e.hasChildren() ? e : null;
} else {
/*
* Map is expected but was something else. Replace the
* whole tuple. Don't touch the whitespace when replacing -
* it looks good
*/
StringBuilder serializedValue = serialize(property, otherValue);
preIndent(serializedValue, map.getStartMark().getColumn());
return createReplaceEditWithoutWhiteSpace(map.getStartMark().getIndex(), tuple.getValueNode().getEndMark().getIndex() - 1, serializedValue.toString().trim());
}
}
}
return null;
}
private StringBuilder serialize(String property, Object value) {
Map<Object, Object> obj = new HashMap<>();
obj.put(property, value);
return new StringBuilder(yaml.dump(obj));
}
private StringBuilder postIndent(StringBuilder s, int offset) {
char[] indent = new char[offset];
for (int i = 0; i < offset; i++) {
indent[i] = ' ';
}
for (int i = 0; i < s.length(); ) {
if (s.charAt(i) == '\n') {
s.insert(i + 1, indent);
i += indent.length;
}
i++;
}
return s;
}
private StringBuilder serializeListEntry(Object obj, int offset) {
StringBuilder s = new StringBuilder(yaml.dump(Collections.singletonList(obj)));
if (offset > 0) {
preIndent(s, offset);
}
return s;
}
private StringBuilder preIndent(StringBuilder s, int offset) {
char[] indent = new char[offset];
for (int i = 0; i < offset; i++) {
indent[i] = ' ';
}
int lineLength = 0;
for (int i = 0; i < s.length(); ) {
if (s.charAt(i) == '\n') {
if (lineLength > 0) {
s.insert(i - lineLength, indent);
i += indent.length;
lineLength = 0;
}
} else {
lineLength++;
}
i++;
}
if (lineLength > 0) {
s.insert(s.length() - lineLength, indent);
lineLength = 0;
}
return s;
}
private DeleteEdit createDeleteEditIncludingLine(int start, int end) {
if (content != null) {
for (; start > 0 && Character.isWhitespace(content.charAt(start - 1)) && content.charAt(start - 1) != '\n'; start--);
for (; end > 0 && end < content.length() && Character.isWhitespace(content.charAt(end)) && content.charAt(end - 1) != '\n'; end++);
}
return new DeleteEdit(start, end - start);
}
private ReplaceEdit createReplaceEditWithoutWhiteSpace(int start, int end, String text) {
for (; start < content.length() && Character.isWhitespace(content.charAt(start)); start++);
for (; end >= start && Character.isWhitespace(content.charAt(end)); end--);
return new ReplaceEdit(start, end - start + 1, text);
}
private int getDefaultOffset() {
if (appNode == null) {
if (applicationsValueNode == null) {
return 0;
} else {
return applicationsValueNode.getStartMark().getColumn();
}
} else {
return appNode.getStartMark().getColumn();
}
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getUris() {
List<Map<?,?>> routes = getAbsoluteValue(ApplicationManifestHandler.ROUTES_PROP, List.class);
if (routes != null) {
return routes.stream()
.map(routeObj -> routeObj.get(ApplicationManifestHandler.ROUTE_PROP))
.filter(o -> o instanceof String)
.map(o -> (String) o)
.collect(Collectors.toSet());
} else {
Boolean noRoute = getAbsoluteValue(ApplicationManifestHandler.NO_ROUTE_PROP, Boolean.class);
if (Boolean.TRUE.equals(noRoute)) {
return Collections.emptySet();
}
List<CFCloudDomain> domains = ApplicationManifestHandler.getCloudDomains(cloudData);
LinkedHashSet<String> hostsSet = new LinkedHashSet<>();
LinkedHashSet<String> domainsSet = new LinkedHashSet<>();
/*
* Gather domains from app node from 'domain' and 'domains' attributes
*/
String domain = getAbsoluteValue(ApplicationManifestHandler.DOMAIN_PROP, String.class);
if (domain != null) {
domainsSet.add(domain);
}
List<?> domainsList = getAbsoluteValue(ApplicationManifestHandler.DOMAINS_PROP, List.class);
if (domainsList != null) {
for (Object o : domainsList) {
if (o instanceof String && ApplicationManifestHandler.isDomainValid((String) o , domains)) {
domainsSet.add((String)o);
}
}
}
/*
* Gather hosts from app node from 'host' and 'hosts'
* attributes.
*/
String host = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAIN_PROP, String.class);
if (host != null) {
hostsSet.add(host);
}
List<?> hostsList = getAbsoluteValue(ApplicationManifestHandler.SUB_DOMAINS_PROP, List.class);
if (hostsList != null) {
for (Object o : hostsList) {
if (o instanceof String) {
hostsSet.add((String)o);
}
}
}
/*
* If no host names found check for "random-route: true" and
* "no-hostname: true" otherwise take app name as the host name
*/
if (hostsSet.isEmpty()) {
Boolean randomRoute = getAbsoluteValue(ApplicationManifestHandler.RANDOM_ROUTE_PROP, Boolean.class);
if (Boolean.TRUE.equals(randomRoute)) {
hostsSet.add(ApplicationManifestHandler.RANDOM_VAR);
domainsSet.clear();
domainsSet.add(domains.get(0).getName());
} else {
Boolean noHostname = getAbsoluteValue(ApplicationManifestHandler.NO_HOSTNAME_PROP, Boolean.class);
if (!Boolean.TRUE.equals(noHostname)) {
hostsSet.add(getAppName());
}
}
}
/*
* Set a domain if they are still empty
*/
if (domainsSet.isEmpty()) {
domainsSet.add(domains.get(0).getName());
}
/*
* Compose URIs for application based on hosts and domains
*/
Set<String> uris = new HashSet<>();
for (String d : domainsSet) {
if (hostsSet.isEmpty()) {
uris.add(CFRoute.builder().domain(d).build().getRoute());
} else {
for (String h : hostsSet) {
uris.add(CFRoute.builder().host(h).domain(d).build().getRoute());
}
}
}
return uris;
}
}
@Override
public int getDiskQuota() {
String quotaStringValue = getAbsoluteValue(ApplicationManifestHandler.DISK_QUOTA_PROP, String.class);
if (quotaStringValue != null) {
try {
return ApplicationManifestHandler.convertMemory(quotaStringValue);
} catch (CoreException e) {
Log.log(e);
}
}
return DeploymentProperties.DEFAULT_MEMORY;
}
public static <V> V getPropertyValue(final Node n, final String key, Class<V> parameter) {
Node node = YamlGraphDeploymentProperties.findValueNode(n, key);
return getValue(node, parameter);
}
@SuppressWarnings("unchecked")
public static <V> V getValue(Node node, Class<V> parameter) {
return (V) new Constructor(parameter) {
@Override
public Object getSingleData(Class<?> type) {
// Ensure that the stream contains a single document and construct it
if (node != null) {
if (type != null) {
node.setTag(new Tag(type));
} else {
node.setTag(rootTag);
}
return constructObject(node);
}
return null;
}
}.getSingleData(parameter);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected <V> V getAbsoluteValue(String key, Class<V> parameter) {
V v = getPropertyValue(appNode, key, parameter);
if (Collection.class.isAssignableFrom(parameter)) {
if (root != appNode) {
V rootV = getPropertyValue(root, key, parameter);
if (rootV != null) {
if (v != null){
((Collection) rootV).addAll((Collection) v);
}
v = rootV;
}
}
} else if (Map.class.isAssignableFrom(parameter)) {
if (root != appNode) {
V rootV = getPropertyValue(root, key, parameter);
if (rootV != null) {
if (v != null){
((Map) rootV).putAll((Map) v);
}
v = rootV;
}
}
} else if (v == null) {
if (root != appNode) {
v = getPropertyValue(root, key, parameter);
}
}
return v;
}
private static boolean isLegacyHostDomainManifestYaml(Node n) {
if (isLegacyHostDomainManifestYamlNode(n)) {
return true;
} else {
Node applicationsObj = findValueNode(n, ApplicationManifestHandler.APPLICATIONS_PROP);
if (applicationsObj instanceof SequenceNode) {
return ((SequenceNode) applicationsObj).getValue().stream()
.map(o -> (Node) o)
.filter(YamlGraphDeploymentProperties::isLegacyHostDomainManifestYamlNode)
.findFirst().isPresent();
}
}
return false;
}
private static boolean isLegacyHostDomainManifestYamlNode(Node node) {
return findValueNode(node, ApplicationManifestHandler.DOMAIN_PROP) != null
|| findValueNode(node, ApplicationManifestHandler.SUB_DOMAIN_PROP) != null
|| findValueNode(node, ApplicationManifestHandler.DOMAINS_PROP) != null
|| findValueNode(node, ApplicationManifestHandler.SUB_DOMAINS_PROP) != null
|| findValueNode(node, ApplicationManifestHandler.NO_HOSTNAME_PROP) != null;
}
}