/******************************************************************************* * Copyright (c) 2014 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; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.TreeMap; import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.ide.eclipse.editor.support.util.FuzzyMatcher; import org.springframework.ide.eclipse.editor.support.util.StringUtil; /** * A collection of data that can be searched with a simple 'fuzzy' string * matching algorithm. Clients must override 'getKey' method to define how * a search 'key' is associated with each data item. * <p> * The collection can then be searched for items who's key matches * simple 'fuzzy' patterns. */ public abstract class FuzzyMap<E> implements Iterable<E> { public static class Match<E> { public double score; public final E data; private String pattern; public Match(String pattern, double score, E e) { this.pattern = pattern; this.score = score; this.data = e; } public static <E> Match<E> getBest(Collection<Match<E>> matches) { double bestScore = Double.NEGATIVE_INFINITY; Match<E> best = null; for (Match<E> match : matches) { if (match.score>bestScore) { best = match; bestScore = match.score; } } return best; } @Override public String toString() { return "Match(score="+score+", data="+data+")"; } public String getPattern() { return pattern; } } @Override public Iterator<E> iterator() { return entries.values().iterator(); } private TreeMap<String,E> entries = new TreeMap<String, E>(); protected abstract String getKey(E entry); public void add(E value) { //This assumes no two entries have the same id. String key = getKey(value); E existing = entries.get(key); if (existing==null) { entries.put(getKey(value), value); } else { SpringPropertiesEditorPlugin.warning(FuzzyMap.class.getName()+": Multiple entries for key "+key+" some entries discarded"); } } /** * Search for pattern. A pattern is just a sequence of characters which have to found in * an entrie's key in the same order as they are in the pattern. * <p> * Note that returned list doesn't yet have elements sorted according to score (instead they * are sorted lexicographically thanks to the fact we use a Tree representation). */ public List<Match<E>> find(String pattern) { if ("".equals(pattern)) { //Special case because // 1) no need to search. Matches everything // 2) want to use different way of sorting / scoring. See https://issuetracker.springsource.com/browse/STS-4008 ArrayList<Match<E>> matches = new ArrayList<Match<E>>(entries.size()); for (E v : entries.values()) { matches.add(new Match<E>(pattern, 1.0, v)); } return matches; } else { //TODO: optimize somehow with a smarter index? (right now searches all map entries sequentially) ArrayList<Match<E>> matches = new ArrayList<Match<E>>(); for (Entry<String, E> e : entries.entrySet()) { String key = e.getKey(); double score = FuzzyMatcher.matchScore(pattern, key); if (score!=0.0) { matches.add(new Match<E>(pattern, score, e.getValue())); } } return matches; } } /** * Searches the index for the longest string which is both * - a prefix of propertyName * - a prefix of some key in the map. * Note: If the map is empty, then this returns null, since * no string, not even the empty string is a prefix of a * key in the map. */ public String findValidPrefix(String propertyName) { E best = findLongestCommonPrefixEntry(propertyName); return best==null?null:StringUtil.commonPrefix(propertyName, getKey(best)); } /** * Find property with longest common prefix for given key. */ public E findLongestCommonPrefixEntry(String propertyName) { //We can implementation this O(log(n)) because the properties are kept in a TreeMap which is sorted. //This means that entries with common prefix will occur 'next to eachother' //The 'best' entry must therefore be either the entry just before or just after //the property we are searching for. Entry<String, E> ceiln = entries.ceilingEntry(propertyName); Entry<String, E> floor = entries.floorEntry(propertyName); Entry<String, E> best; if (floor==null || floor==ceiln) { best = ceiln; } else if (ceiln==null) { best = floor; } else { int floorScore = floor==null?0:StringUtil.commonPrefixLength(floor.getKey(), propertyName); int ceilnScore = ceiln==null?0:StringUtil.commonPrefixLength(ceiln.getKey(), propertyName); best = floorScore>ceilnScore ? floor : ceiln; } return best==null?null:best.getValue(); } /** * Find an exact match if it exists. */ public E get(String id) { return entries.get(id); } public boolean isEmpty() { return entries==null || entries.isEmpty(); } public int size() { return entries.size(); } }