/******************************************************************************* * Copyright (c) 2015, 2016 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.properties.editor.yaml.reconcile; import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertiesProblemType.YAML_DEPRECATED; import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertiesProblemType.YAML_DUPLICATE_KEY; import static org.springframework.ide.eclipse.editor.support.yaml.ast.NodeUtil.asScalar; import static org.springframework.ide.eclipse.editor.support.yaml.ast.YamlFileAST.getChildren; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.IProgressMonitor; import org.springframework.ide.eclipse.boot.properties.editor.metadata.PropertyInfo; import org.springframework.ide.eclipse.boot.properties.editor.quickfix.ReplaceDeprecatedPropertyQuickfix; import org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertiesProblemType; import org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertyProblem; import org.springframework.ide.eclipse.boot.properties.editor.util.Type; import org.springframework.ide.eclipse.boot.properties.editor.util.TypeParser; import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil; import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil.BeanPropertyNameMode; import org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil.EnumCaseMode; import org.springframework.ide.eclipse.boot.properties.editor.util.TypedProperty; import org.springframework.ide.eclipse.editor.support.reconcile.IProblemCollector; import org.springframework.ide.eclipse.editor.support.util.StringUtil; import org.springframework.ide.eclipse.editor.support.util.ValueParser; import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeRef; import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeRef.Kind; import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeRef.TupleValueRef; import org.springframework.ide.eclipse.editor.support.yaml.ast.NodeUtil; import org.springframework.ide.eclipse.editor.support.yaml.ast.YamlFileAST; import org.springframework.ide.eclipse.editor.support.yaml.reconcile.YamlASTReconciler; import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; import org.yaml.snakeyaml.nodes.NodeTuple; import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.SequenceNode; /** * @author Kris De Volder */ public class ApplicationYamlASTReconciler implements YamlASTReconciler { private final IProblemCollector problems; private final TypeUtil typeUtil; private final IndexNavigator nav; public ApplicationYamlASTReconciler(IProblemCollector problems, IndexNavigator nav, TypeUtil typeUtil) { this.problems = problems; this.typeUtil = typeUtil; this.nav = nav; } @Override public void reconcile(YamlFileAST ast, IProgressMonitor mon) { reconcile(ast, nav, mon); } protected void reconcile(YamlFileAST ast, IndexNavigator nav, IProgressMonitor mon) { List<Node> nodes = ast.getNodes(); if (nodes!=null && !nodes.isEmpty()) { mon.beginTask("Reconcile", nodes.size()); try { for (Node node : nodes) { reconcile(node, nav); mon.worked(1); } } finally { mon.done(); } } } protected void reconcile(Node node, IndexNavigator nav) { switch (node.getNodeId()) { case mapping: checkForDuplicateKeys((MappingNode)node); for (NodeTuple entry : ((MappingNode)node).getValue()) { reconcile(entry, nav); } break; case scalar: if (!isIgnoreScalarAssignmentTo(nav.getPrefix())) { expectMapping(node); } break; default: expectMapping(node); break; } } private void checkForDuplicateKeys(MappingNode node) { Set<String> duplicateKeys = new HashSet<>(); Set<String> seenKeys = new HashSet<>(); for (NodeTuple entry : node.getValue()) { String key = asScalar(entry.getKeyNode()); if (key!=null) { if (!seenKeys.add(key)) { duplicateKeys.add(key); } } } if (!duplicateKeys.isEmpty()) { for (NodeTuple entry : node.getValue()) { Node keyNode = entry.getKeyNode(); String key = asScalar(keyNode); if (key!=null && duplicateKeys.contains(key)) { problems.accept(problem(YAML_DUPLICATE_KEY, keyNode, "Duplicate key '"+key+"'")); } } } } protected boolean isIgnoreScalarAssignmentTo(String propName) { //See https://issuetracker.springsource.com/browse/STS-4144 return propName!=null && propName.equals("spring.profiles"); } private void reconcile(NodeTuple entry, IndexNavigator nav) { Node keyNode = entry.getKeyNode(); String key = asScalar(keyNode); if (key==null) { expectScalar(keyNode); } else { IndexNavigator subNav = nav.selectSubProperty(key); PropertyInfo match = subNav.getExactMatch(); PropertyInfo extension = subNav.getExtensionCandidate(); if (match==null && extension==null) { //nothing found for this key. Maybe user is using camelCase variation of the key? String keyAlias = StringUtil.camelCaseToHyphens(key); IndexNavigator subNavAlias = nav.selectSubProperty(keyAlias); match = subNavAlias.getExactMatch(); extension = subNavAlias.getExtensionCandidate(); if (match!=null || extension!=null) { //Got something for the alias, so use that instead. //Note: do not swap for alias unless we actually found something. // This gives more logical errors (in terms of user's key, not its canonical alias) subNav = subNavAlias; } } if (match!=null && extension!=null) { //This is an odd situation, the current prefix lands on a propery //but there are also other properties that have it as a prefix. //This ambiguity is hard to deal with and we choose not to do so for now return; } else if (match!=null) { Type type = TypeParser.parse(match.getType()); if (match.isDeprecated()) { deprecatedProperty(match, keyNode); } reconcile(entry.getValueNode(), type); } else if (extension!=null) { //We don't really care about the extension only about the fact that it // exists and so it is meaningful to continue checking... Node valueNode = entry.getValueNode(); reconcile(valueNode, subNav); } else { //both are null, this means there's no valid property with the current prefix //whether exact or extending it with further navigation unkownProperty(keyNode, subNav.getPrefix(), entry); } } } /** * Reconcile a node given the type that we expect the node to be. */ private void reconcile(Node node, Type type) { if (type!=null) { switch (node.getNodeId()) { case scalar: reconcile((ScalarNode)node, type); break; case sequence: reconcile((SequenceNode)node, type); break; case mapping: reconcile((MappingNode)node, type); break; case anchor: //TODO: what should we do with anchor nodes break; default: throw new IllegalStateException("Missing switch case"); } } } private void reconcile(MappingNode mapping, Type type) { checkForDuplicateKeys(mapping); if (typeUtil.isAtomic(type)) { expectTypeFoundMapping(type, mapping); } else if (TypeUtil.isMap(type) || TypeUtil.isSequencable(type)) { Type keyType = typeUtil.getKeyType(type); Type valueType = TypeUtil.getDomainType(type); if (keyType!=null) { for (NodeTuple entry : mapping.getValue()) { reconcile(entry.getKeyNode(), keyType); } } if (valueType!=null) { for (NodeTuple entry : mapping.getValue()) { Node value = entry.getValueNode(); Type nestedValueType = valueType; if (value.getNodeId()==NodeId.mapping) { //Some special cases to handle here!! // See https://issuetracker.springsource.com/browse/STS-4254 // See https://issuetracker.springsource.com/browse/STS-4335 if (TypeUtil.isObject(valueType)) { nestedValueType = type; } else if (TypeUtil.isString(keyType) && typeUtil.isAtomic(valueType)) { nestedValueType = type; } } reconcile(entry.getValueNode(), nestedValueType); } } } else { // Neither atomic, map or sequence-like => bean-like Map<String, TypedProperty> props = typeUtil.getPropertiesMap(type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED); if (props!=null) { for (NodeTuple entry : mapping.getValue()) { Node keyNode = entry.getKeyNode(); String key = NodeUtil.asScalar(keyNode); if (key==null) { expectBeanPropertyName(keyNode, type); } else { if (!props.containsKey(key)) { unknownBeanProperty(keyNode, type, key); } else { Node valNode = entry.getValueNode(); TypedProperty typedProperty = props.get(key); if (typedProperty!=null) { if (typedProperty.isDeprecated()) { deprecatedProperty(type, typedProperty, keyNode); } reconcile(valNode, typedProperty.getType()); } } } } } } } private void reconcile(SequenceNode seq, Type type) { if (typeUtil.isAtomic(type)) { expectTypeFoundSequence(type, seq); } else if (TypeUtil.isSequencable(type)) { Type domainType = TypeUtil.getDomainType(type); if (domainType!=null) { for (Node element : seq.getValue()) { reconcile(element, domainType); } } } else { expectTypeFoundSequence(type, seq); } } private void reconcile(ScalarNode scalar, Type type) { String stringValue = scalar.getValue(); if (!stringValue.contains("${")) { //don't check anything with ${} expressions in it as we // don't know its actual value ValueParser valueParser = typeUtil.getValueParser(type); if (valueParser!=null) { // Tag tag = scalar.getTag(); //use the tag? Actually, boot tolerates String values // even if integeger etc are expected. It has its ways of parsing the String to the // expected type try { valueParser.parse(stringValue); } catch (Exception e) { //Couldn't parse valueTypeMismatch(type, scalar); } } } } private void expectTypeFoundMapping(Type type, MappingNode node) { expectType(SpringPropertiesProblemType.YAML_EXPECT_TYPE_FOUND_MAPPING, type, node); } private void expectTypeFoundSequence(Type type, SequenceNode seq) { expectType(SpringPropertiesProblemType.YAML_EXPECT_TYPE_FOUND_SEQUENCE, type, seq); } private void valueTypeMismatch(Type type, ScalarNode scalar) { expectType(SpringPropertiesProblemType.YAML_VALUE_TYPE_MISMATCH, type, scalar); } private void unkownProperty(Node node, String name, NodeTuple entry) { SpringPropertyProblem p = problem(SpringPropertiesProblemType.YAML_UNKNOWN_PROPERTY, node, "Unknown property '"+name+"'"); p.setPropertyName(extendForQuickfix(StringUtil.camelCaseToHyphens(name), entry.getValueNode())); problems.accept(p); } private String extendForQuickfix(String name, Node node) { if (node!=null) { TupleValueRef child = getFirstTupleValue(getChildren(node)); if (child!=null) { String extra = NodeUtil.asScalar(child.getKey()); if (extra!=null) { return extendForQuickfix(name + "." + StringUtil.camelCaseToHyphens(extra), child.get()); } } } //couldn't extend name any further return name; } private TupleValueRef getFirstTupleValue(List<NodeRef<?>> children) { for (NodeRef<?> nodeRef : children) { if (nodeRef.getKind()==Kind.VAL) { return (TupleValueRef) nodeRef; } } return null; } private void expectScalar(Node node) { problems.accept(problem(SpringPropertiesProblemType.YAML_EXPECT_SCALAR, node, "Expecting a 'Scalar' node but got "+describe(node))); } protected void expectMapping(Node node) { problems.accept(problem(SpringPropertiesProblemType.YAML_EXPECT_MAPPING, node, "Expecting a 'Mapping' node but got "+describe(node))); } private void expectBeanPropertyName(Node keyNode, Type type) { problems.accept(problem(SpringPropertiesProblemType.YAML_EXPECT_BEAN_PROPERTY_NAME, keyNode, "Expecting a bean-property name for object of type '"+typeUtil.niceTypeName(type)+"' " + "but got "+describe(keyNode))); } private void unknownBeanProperty(Node keyNode, Type type, String name) { problems.accept(problem(SpringPropertiesProblemType.YAML_INVALID_BEAN_PROPERTY, keyNode, "Unknown property '"+name+"' for type '"+typeUtil.niceTypeName(type)+"'")); } private void expectType(SpringPropertiesProblemType problemType, Type type, Node node) { problems.accept(problem(problemType, node, "Expecting a '"+typeUtil.niceTypeName(type)+"' but got "+describe(node))); } private void deprecatedProperty(PropertyInfo property, Node keyNode) { SpringPropertyProblem problem = deprecatedPropertyProblem(property.getId(), null, keyNode, property.getDeprecationReplacement(), property.getDeprecationReason()); problem.setMetadata(property); problem.setProblemFixer(ReplaceDeprecatedYamlQuickfix.FIXER); problems.accept(problem); } private void deprecatedProperty(Type contextType, TypedProperty property, Node keyNode) { SpringPropertyProblem problem = deprecatedPropertyProblem(property.getName(), typeUtil.niceTypeName(contextType), keyNode, property.getDeprecationReplacement(), property.getDeprecationReason()); problems.accept(problem); } protected SpringPropertyProblem deprecatedPropertyProblem(String name, String contextType, Node keyNode, String replace, String reason) { SpringPropertyProblem problem = problem(YAML_DEPRECATED, keyNode, TypeUtil.deprecatedPropertyMessage(name, contextType, replace, reason)); problem.setPropertyName(name); return problem; } protected SpringPropertyProblem problem(SpringPropertiesProblemType type, Node node, String msg) { int start = node.getStartMark().getIndex(); int end = node.getEndMark().getIndex(); return SpringPropertyProblem.problem(type, msg, start, end-start); } private String describe(Node node) { switch (node.getNodeId()) { case scalar: return "'"+((ScalarNode)node).getValue()+"'"; case mapping: return "a 'Mapping' node"; case sequence: return "a 'Sequence' node"; case anchor: return "a 'Anchor' node"; default: throw new IllegalStateException("Missing switch case"); } } }