/******************************************************************************* * Copyright (c) 2014-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.reconciling; import static org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesCompletionEngine.isAssign; import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertiesProblemType.PROP_DEPRECATED; import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertiesProblemType.PROP_UNKNOWN_PROPERTY; import static org.springframework.ide.eclipse.boot.properties.editor.reconciling.SpringPropertyProblem.problem; import static org.springframework.ide.eclipse.editor.support.util.StringUtil.commonPrefix; import java.util.Map; import java.util.regex.Pattern; import javax.inject.Provider; import javax.print.Doc; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.internal.ui.propertiesfileeditor.IPropertiesFilePartitions; import org.eclipse.jdt.internal.ui.propertiesfileeditor.PropertiesFileEscapes; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextUtilities; import org.springframework.ide.eclipse.boot.properties.editor.FuzzyMap; import org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesCompletionEngine; import org.springframework.ide.eclipse.boot.properties.editor.SpringPropertiesEditorPlugin; 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.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.editor.support.reconcile.IProblemCollector; import org.springframework.ide.eclipse.editor.support.reconcile.IReconcileEngine; import org.springframework.ide.eclipse.editor.support.util.DocumentRegion; import org.springframework.ide.eclipse.editor.support.util.DocumentUtil; import org.springframework.ide.eclipse.editor.support.util.ValueParser; /** * Implements reconciling algorithm for {@link SpringPropertiesReconcileStrategy}. * <p> * The code in here could have been also part of the {@link SpringPropertiesReconcileStrategy} * itself, however isolating it here allows it to me more easily unit tested (no dependencies * on ISourceViewer which is difficult to 'mock' in testing harness. * * @author Kris De Volder */ @SuppressWarnings("restriction") public class SpringPropertiesReconcileEngine implements IReconcileEngine { /** * Regexp that matches a ',' surrounded by whitespace, including escaped whitespace / newlines */ private static final Pattern COMMA = Pattern.compile( "(\\s|\\\\\\s)*,(\\s|\\\\\\s)*" ); private static final Pattern SPACES = Pattern.compile( "(\\s|\\\\\\s)*" ); /** * Regexp that matches a whitespace, including escaped whitespace */ private static final Pattern ASSIGN = SpringPropertiesCompletionEngine.ASSIGN; private Provider<FuzzyMap<PropertyInfo>> fIndexProvider; private TypeUtil typeUtil; private final DelimitedListReconciler commaListReconciler = new DelimitedListReconciler(COMMA, this::reconcileType); public SpringPropertiesReconcileEngine(Provider<FuzzyMap<PropertyInfo>> provider, TypeUtil typeUtil) { this.fIndexProvider = provider; this.typeUtil = typeUtil; } public void reconcile(IDocument doc, IProblemCollector problemCollector, IProgressMonitor mon) { FuzzyMap<PropertyInfo> index = getIndex(); if (index==null || index.isEmpty()) { //don't report errors when index is empty, simply don't check (otherwise we will just reprot // all properties as errors, but this not really useful information since the cause is // some problem putting information about properties into the index. return; } problemCollector.beginCollecting(); try { DuplicateNameChecker duplicateNameChecker = new DuplicateNameChecker(problemCollector); ITypedRegion[] regions = TextUtilities.computePartitioning(doc, IPropertiesFilePartitions.PROPERTIES_FILE_PARTITIONING, 0, doc.getLength(), true); if (regions!=null && regions.length>0) { mon.beginTask("Reconciling Spring Properties", regions.length); for (int i = 0; i < regions.length; i++) { ITypedRegion r = regions[i]; try { String type = r.getType(); if (IDocument.DEFAULT_CONTENT_TYPE.equals(type)) { DocumentRegion fullName = new DocumentRegion(doc, r).trim(); if (fullName.isEmpty()) { if (!isAssigned(doc, r)) { //empty 'properties' are okay if not being assigned to. This just means that // there are empty sections in the props file and this is okay. continue; } } duplicateNameChecker.check(fullName); PropertyInfo validProperty = SpringPropertiesCompletionEngine.findLongestValidProperty(index, fullName.toString()); if (validProperty!=null) { //TODO: Remove last remnants of 'IRegion trimmedRegion' here and replace // it all with just passing around 'fullName' DocumentRegion. This may require changes // in PropertyNavigator (probably these changes are also for the better making it simpler as well) IRegion trimmedRegion = fullName.asRegion(); if (validProperty.isDeprecated()) { problemCollector.accept(problemDeprecated(fullName, validProperty)); } int offset = validProperty.getId().length() + trimmedRegion.getOffset(); PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtil, trimmedRegion); Type valueType = navigator.navigate(offset, TypeParser.parse(validProperty.getType())); if (valueType!=null) { reconcileType(doc, valueType, regions, i, problemCollector); } } else { //validProperty==null //The name is invalid, with no 'prefix' of the name being a valid property name. PropertyInfo similarEntry = index.findLongestCommonPrefixEntry(fullName.toString()); CharSequence validPrefix = commonPrefix(similarEntry.getId(), fullName); problemCollector.accept(problemUnkownProperty(fullName, similarEntry, validPrefix)); } //end: validProperty==null } } catch (Exception e) { SpringPropertiesEditorPlugin.log(e); } } //end: for regions } } catch (Throwable e2) { SpringPropertiesEditorPlugin.log(e2); } finally { problemCollector.endCollecting(); } } protected SpringPropertyProblem problemDeprecated(DocumentRegion trimmedRegion, PropertyInfo property) { SpringPropertyProblem p = problem(PROP_DEPRECATED, TypeUtil.deprecatedPropertyMessage( property.getId(), null, property.getDeprecationReplacement(), property.getDeprecationReason() ), trimmedRegion ); p.setPropertyName(property.getId()); p.setMetadata(property); p.setProblemFixer(ReplaceDeprecatedPropertyQuickfix.FIXER); return p; } protected SpringPropertyProblem problemUnkownProperty(DocumentRegion fullNameRegion, PropertyInfo similarEntry, CharSequence validPrefix) { String fullName = fullNameRegion.toString(); SpringPropertyProblem p = problem(PROP_UNKNOWN_PROPERTY, "'"+fullName+"' is an unknown property."+suggestSimilar(similarEntry, validPrefix, fullName), fullNameRegion.subSequence(validPrefix.length()) ); p.setPropertyName(fullName); return p; } private FuzzyMap<PropertyInfo> getIndex() { return fIndexProvider.get(); } private void reconcileType(IDocument doc, Type expectType, ITypedRegion[] regions, int i, IProblemCollector problems) { DocumentRegion escapedValue = getAssignedValue(doc, regions, i); if (escapedValue==null) { int charPos = DocumentUtil.lastNonWhitespaceCharOfRegion(doc, regions[i]); if (charPos>=0) { problems.accept(problem(SpringPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH, "Expecting '"+typeUtil.niceTypeName(expectType)+"'", charPos, 1)); } } else { reconcileType(escapedValue, expectType, problems); } } private void reconcileType(DocumentRegion escapedValue, Type expectType, IProblemCollector problems) { ValueParser parser = typeUtil.getValueParser(expectType); if (parser!=null) { try { String valueStr = PropertiesFileEscapes.unescape(escapedValue.toString()); if (!valueStr.contains("${")) { //Don't check strings that look like they use variable substitution. parser.parse(valueStr); } } catch (Exception e) { problems.accept(problem(SpringPropertiesProblemType.PROP_VALUE_TYPE_MISMATCH, "Expecting '"+typeUtil.niceTypeName(expectType)+"'", escapedValue)); } } else if (TypeUtil.isList(expectType)||TypeUtil.isArray(expectType)) { commaListReconciler.reconcile(escapedValue, expectType, problems); } } private DocumentRegion getAssignedValue(IDocument doc, ITypedRegion[] regions, int i) { int valueRegionIndex = i+1; if (valueRegionIndex<regions.length) { String valueRegionType = regions[valueRegionIndex].getType(); DocumentRegion valueRegion = new DocumentRegion(doc, regions[valueRegionIndex]); if (IPropertiesFilePartitions.PROPERTY_VALUE.equals(valueRegionType)) { //Need to remove the 'ASSIGN' bit from the start valueRegion = valueRegion.trimStart(ASSIGN).trimEnd(SPACES); //region text includes // potential padding with whitespace. // the ':' or '=' (if its there). return valueRegion; } } return null; } private String suggestSimilar(PropertyInfo similarEntry, CharSequence validPrefix, CharSequence fullName) { int matchedChars = validPrefix.length(); int wrongChars = fullName.length()-matchedChars; if (wrongChars<matchedChars) { return " Did you mean '"+similarEntry.getId()+"'?"; } else { return ""; } } /** * Check that there is an assignment char directly following the given region. */ private boolean isAssigned(IDocument doc, IRegion r) { try { char c = doc.getChar(r.getOffset()+r.getLength()); //Note either a '=' or a ':' can be used to assign properties. return isAssign(c); } catch (BadLocationException e) { //happens if looking for assignment char outside the document return false; } } }