/*******************************************************************************
* Copyright (c) 2015 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.reconciling;
import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertyProblem.problem;
import static org.springframework.ide.eclipse.boot.properties.editor.util.TypeUtil.isBracketable;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesCompletionEngine;
import org.springframework.ide.eclipse.boot.properties.editor.util.Type;
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.reconcile.ReconcileProblem;
import org.springframework.ide.eclipse.editor.support.util.StringUtil;
import org.springframework.ide.eclipse.editor.support.util.ValueParser;
/**
* Helper class for {@link SpringPropertiesReconcileEngine} and {@link SpringPropertiesCompletionEngine}.
* <p>
* This class provides a means to 'navigate' a chain of bracket and dot navigation operations down
* from a typed property into its value type.
*
* @author Kris De Volder
*/
public class PropertyNavigator {
private static final char EOF = 0;
/**
* If problem collector is not null, then problems detected in the navigation chain are added to the
* collector.
*/
private IProblemCollector problemCollector;
/**
* Document in which navigation chain text is contained.
*/
private IDocument doc;
private TypeUtil typeUtil;
private IRegion region;
private String regionText;
public PropertyNavigator(IDocument doc, IProblemCollector problemCollector, TypeUtil typeUtil, IRegion region) throws BadLocationException {
this.doc = doc;
this.problemCollector = problemCollector==null?IProblemCollector.NULL:problemCollector;
this.typeUtil = typeUtil;
this.region = region;
this.regionText = doc.get(region.getOffset(), region.getLength());
}
/**
* @param offset current position in the nav chain. Text before this offset is already 'processed'.
* @param type The type at the end of the already processed nav chain. The next nav op in the chain
* should go deeper into this type.
* @param r The entire region of the navchain, including both the already processed portion as well
* as the remaining text.
* @return Type at the end of the whole nav chain, or null if the type could not be determined.
*/
public Type navigate(int offset, Type type) {
if (type!=null) {
if (offset<getEnd(region)) {
char navOp = getChar(offset);
if (navOp=='.') {
if (typeUtil.isDotable(type)) {
return dotNavigate(offset, type);
} else {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_INVALID_BEAN_NAVIGATION,
"Can't use '.' navigation for property '"+textBetween(region.getOffset(), offset)+"' of type "+type,
offset, getEnd(region)-offset));
}
} else if (navOp=='[') {
if (isBracketable(type)) {
return bracketNavigate(offset, type);
} else {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_INVALID_INDEXED_NAVIGATION,
"Can't use '[..]' navigation for property '"+textBetween(region.getOffset(), offset)+"' of type "+type,
offset, getEnd(region)-offset));
}
} else {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_EXPECTED_DOT_OR_LBRACK, "Expecting either a '.' or '['", offset, getEnd(region)-offset));
}
} else {
//end of nav chain
return type;
}
}
//Something we can't handle...
return null;
}
private String textBetween(int start, int end) {
try {
if (end>start) {
return doc.get(start, end-start);
}
} catch (BadLocationException e) {
//ignore
}
return "";
}
private int indexOf(char c, int from) {
int offset = region.getOffset();
int found = regionText.indexOf(c, from-offset);
if (found>=0) {
return found+offset;
}
return -1;
}
/**
* Handle bracket navigation into given type, after a bracket at
* was found at given offset. Assumes the type has already been checked to
* be 'bracketable'.
*/
private Type bracketNavigate(int offset, Type type) {
int lbrack = offset;
int rbrack = indexOf(']', lbrack);
if (rbrack<0) {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_NO_MATCHING_RBRACK,
"No matching ']'",
offset, 1));
} else {
String indexStr = textBetween(lbrack+1, rbrack);
if (!indexStr.contains("${")) {
try {
Integer.parseInt(indexStr);
} catch (Exception e) {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_NON_INTEGER_IN_BRACKETS,
"Expecting 'Integer' for '[...]' notation '"+textBetween(region.getOffset(), lbrack)+"'",
lbrack+1, rbrack-lbrack-1
));
}
}
Type domainType = TypeUtil.getDomainType(type);
return navigate(rbrack+1, domainType);
}
return null;
}
/**
* Handle dot navigation into given type, after a '.' was
* was found at given offset. Assumes the type has already been
* checked to be 'dotable'.
*/
private Type dotNavigate(int offset, Type type) {
if (TypeUtil.isMap(type)) {
int keyStart = offset+1;
Type domainType = TypeUtil.getDomainType(type);
int keyEnd = -1;
if (typeUtil.isDotable(domainType)) {
//'.' should be interpreted as navigation.
keyEnd = nextNavOp(".[", offset+1);
} else {
//'.' should *not* be interpreted as navigation.
keyEnd = nextNavOp("[", offset+1);
}
String key = textBetween(keyStart, keyEnd);
Type keyType = typeUtil.getKeyType(type);
if (keyType!=null) {
ValueParser keyParser = typeUtil.getValueParser(keyType);
if (keyParser!=null) {
try {
keyParser.parse(key);
} catch (Exception e) {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH,
"Expecting "+typeUtil.niceTypeName(keyType),
keyStart, keyEnd-keyStart));
}
}
}
return navigate(keyEnd, domainType);
} else {
// dot navigation into object properties
int keyStart = offset+1;
int keyEnd = nextNavOp(".[", offset+1);
if (keyEnd<0) {
keyEnd = getEnd(region);
}
String key = StringUtil.camelCaseToHyphens(textBetween(keyStart, keyEnd));
List<TypedProperty> properties = typeUtil.getProperties(type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
if (properties!=null) {
TypedProperty prop = null;
for (TypedProperty p : properties) {
if (p.getName().equals(key)) {
prop = p;
break;
}
}
if (prop==null) {
problemCollector.accept(problem(SpringPropertiesProblemType.PROP_INVALID_BEAN_PROPERTY,
"Type '"+typeUtil.niceTypeName(type)+"' has no property '"+key+"'",
keyStart, keyEnd-keyStart));
} else {
if (prop.isDeprecated()) {
problemCollector.accept(problemDeprecated(type, prop, keyStart, keyEnd-keyStart));
}
return navigate(keyEnd, prop.getType());
}
}
}
return null;
}
private ReconcileProblem problemDeprecated(Type contextType, TypedProperty prop, int offset, int len) {
SpringPropertyProblem p = problem(SpringPropertiesProblemType.PROP_DEPRECATED,
TypeUtil.deprecatedPropertyMessage(
prop.getName(), typeUtil.niceTypeName(contextType),
prop.getDeprecationReplacement(), prop.getDeprecationReason()
),
offset, len
);
p.setPropertyName(prop.getName());
return p;
}
/**
* Skip ahead from give position until reaching the next 'navigation' operator (or the end
* of the navigation chain region).
*
* @param navops Each character in this string is considered a 'navigation operator'.
* @param pos current position in the document.
* @return position of next navop if found, or the position at the end of the region if not found.
*/
private int nextNavOp(String navops, int pos) {
int end = getEnd(region);
while (pos < end && navops.indexOf(getChar(pos))<0) {
pos++;
}
return Math.min(pos, end); //ensure never past the end
}
private char getChar(int offset) {
try {
return doc.getChar(offset);
} catch (BadLocationException e) {
//outside doc, return something anyways.
return EOF;
}
}
private int getEnd(IRegion region) {
return region.getOffset()+region.getLength();
}
}