/*******************************************************************************
* 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.editor.support.yaml.reconcile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IProgressMonitor;
import org.springframework.ide.eclipse.editor.support.reconcile.IProblemCollector;
import org.springframework.ide.eclipse.editor.support.util.ValueParser;
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.schema.YType;
import org.springframework.ide.eclipse.editor.support.yaml.schema.YTypeUtil;
import org.springframework.ide.eclipse.editor.support.yaml.schema.YTypedProperty;
import org.springframework.ide.eclipse.editor.support.yaml.schema.YamlSchema;
import org.springframework.util.StringUtils;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
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;
public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
private final IProblemCollector problems;
private final YamlSchema schema;
private final YTypeUtil typeUtil;
public SchemaBasedYamlASTReconciler(IProblemCollector problems, YamlSchema schema) {
this.problems = problems;
this.schema = schema;
this.typeUtil = schema.getTypeUtil();
}
@Override
public void reconcile(YamlFileAST ast, IProgressMonitor mon) {
List<Node> nodes = ast.getNodes();
if (nodes!=null && !nodes.isEmpty()) {
mon.beginTask("Reconcile", nodes.size());
try {
for (Node node : nodes) {
reconcile(node, schema.getTopLevelType());
mon.worked(1);
}
} finally {
mon.done();
}
}
}
private void reconcile(Node node, YType type) {
if (type!=null) {
switch (node.getNodeId()) {
case mapping:
MappingNode map = (MappingNode) node;
if (typeUtil.isMap(type)) {
for (NodeTuple entry : map.getValue()) {
reconcile(entry.getKeyNode(), typeUtil.getKeyType(type));
reconcile(entry.getValueNode(), typeUtil.getDomainType(type));
}
} else if (typeUtil.isBean(type)) {
Map<String, YTypedProperty> beanProperties = typeUtil.getPropertiesMap(type);
for (NodeTuple entry : map.getValue()) {
Node keyNode = entry.getKeyNode();
String key = NodeUtil.asScalar(keyNode);
if (key==null) {
expectScalar(node);
} else {
YTypedProperty prop = beanProperties.get(key);
if (prop==null) {
unknownBeanProperty(keyNode, type, key);
} else {
reconcile(entry.getValueNode(), prop.getType());
}
}
}
} else {
expectTypeButFoundMap(type, node);
}
break;
case sequence:
SequenceNode seq = (SequenceNode) node;
if (typeUtil.isSequencable(type)) {
for (Node el : seq.getValue()) {
reconcile(el, typeUtil.getDomainType(type));
}
} else {
expectTypeButFoundSequence(type, node);
}
break;
case scalar:
if (typeUtil.isAtomic(type)) {
ValueParser parser = typeUtil.getValueParser(type);
if (parser!=null) {
try {
parser.parse(NodeUtil.asScalar(node));
} catch (Exception e) {
String msg = ExceptionUtil.getMessage(e);
valueParseError(type, node, msg);
}
}
} else {
expectTypeButFoundScalar(type, node);
}
break;
default:
// other stuff we don't check
}
}
}
private void valueParseError(YType type, Node node, String parseErrorMsg) {
String msg= "Couldn't parse as '"+describe(type)+"'";
if (StringUtils.hasText(parseErrorMsg)) {
msg += " ("+parseErrorMsg+")";
}
problem(node, msg);
}
private void unknownBeanProperty(Node keyNode, YType type, String name) {
problem(keyNode, "Unknown property '"+name+"' for type '"+typeUtil.niceTypeName(type)+"'");
}
private void expectScalar(Node node) {
problem(node, "Expecting a 'Scalar' node but got "+describe(node));
}
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");
}
}
private void expectTypeButFoundScalar(YType type, Node node) {
problem(node, "Expecting a '"+describe(type)+"' but found a 'Scalar'");
}
private void expectTypeButFoundSequence(YType type, Node node) {
problem(node, "Expecting a '"+describe(type)+"' but found a 'Sequence'");
}
private void expectTypeButFoundMap(YType type, Node node) {
problem(node, "Expecting a '"+describe(type)+"' but found a 'Map'");
}
private String describe(YType type) {
if (typeUtil.isAtomic(type)) {
return typeUtil.niceTypeName(type);
}
ArrayList<String> expectedNodeTypes = new ArrayList<>();
if (typeUtil.isBean(type) || typeUtil.isMap(type)) {
expectedNodeTypes.add("Map");
}
if (typeUtil.isSequencable(type)) {
expectedNodeTypes.add("Sequence");
}
return StringUtils.collectionToDelimitedString(expectedNodeTypes, " or ");
}
private void problem(Node node, String msg) {
problems.accept(YamlSchemaProblems.schemaProblem(msg, node));
}
}