001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to you under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package gudusoft.gsqlparser.ext.sqlnamematcher;
018
019import java.util.ArrayList;
020import java.util.LinkedHashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.TreeSet;
025
026import static java.util.Objects.requireNonNull;
027
028/**
029 * Helpers for {@link SqlNameMatcher}.
030 *
031 * <p>Originally derived from Apache Calcite's name matching framework.
032 */
033public class SqlNameMatchers {
034
035  private static final BaseMatcher CASE_SENSITIVE = new BaseMatcher(true);
036  private static final BaseMatcher CASE_INSENSITIVE = new BaseMatcher(false);
037
038  private SqlNameMatchers() {}
039
040  /** Returns a name matcher with the given case sensitivity. */
041  public static SqlNameMatcher withCaseSensitive(final boolean caseSensitive) {
042    return caseSensitive ? CASE_SENSITIVE : CASE_INSENSITIVE;
043  }
044
045  /** Creates a name matcher that can suggest corrections to what the user
046   * typed. It matches liberally (case-insensitively) and also records the last
047   * match. */
048  public static SqlNameMatcher liberal() {
049    return new LiberalNameMatcher();
050  }
051
052  /** Partial implementation of {@link SqlNameMatcher}. */
053  private static class BaseMatcher implements SqlNameMatcher {
054    private final boolean caseSensitive;
055
056    BaseMatcher(boolean caseSensitive) {
057      this.caseSensitive = caseSensitive;
058    }
059
060    @Override public boolean isCaseSensitive() {
061      return caseSensitive;
062    }
063
064    @Override public boolean matches(String string, String name) {
065      return caseSensitive ? string.equals(name)
066          : string.equalsIgnoreCase(name);
067    }
068
069    protected boolean listMatches(List<String> list0, List<String> list1) {
070      if (list0.size() != list1.size()) {
071        return false;
072      }
073      for (int i = 0; i < list0.size(); i++) {
074        String s0 = list0.get(i);
075        String s1 = list1.get(i);
076        if (!matches(s0, s1)) {
077          return false;
078        }
079      }
080      return true;
081    }
082
083    @Override public <K extends List<String>, V> V get(Map<K, V> map,
084        List<String> prefixNames, List<String> names) {
085      final List<String> key = concat(prefixNames, names);
086      if (caseSensitive) {
087        //noinspection SuspiciousMethodCalls
088        return map.get(key);
089      }
090      for (Map.Entry<K, V> entry : map.entrySet()) {
091        if (listMatches(key, entry.getKey())) {
092          matched(prefixNames, entry.getKey());
093          return entry.getValue();
094        }
095      }
096      return null;
097    }
098
099    private static List<String> concat(List<String> prefixNames, List<String> names) {
100      if (prefixNames.isEmpty()) {
101        return names;
102      } else {
103        List<String> result = new ArrayList<>(prefixNames.size() + names.size());
104        result.addAll(prefixNames);
105        result.addAll(names);
106        return result;
107      }
108    }
109
110    protected void matched(List<String> prefixNames, List<String> names) {
111    }
112
113    protected List<String> bestMatch() {
114      throw new UnsupportedOperationException();
115    }
116
117    @Override public String bestString() {
118      return listToString(bestMatch());
119    }
120
121    @Override public int frequency(Iterable<String> names, String name) {
122      int n = 0;
123      for (String s : names) {
124        if (matches(s, name)) {
125          ++n;
126        }
127      }
128      return n;
129    }
130
131    @Override public Set<String> createSet() {
132      return isCaseSensitive()
133          ? new LinkedHashSet<>()
134          : new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
135    }
136  }
137
138  /** Matcher that remembers the requests that were made of it. */
139  private static class LiberalNameMatcher extends BaseMatcher {
140    List<String> matchedNames;
141
142    LiberalNameMatcher() {
143      super(false);
144    }
145
146    @Override protected boolean listMatches(List<String> list0,
147        List<String> list1) {
148      final boolean b = super.listMatches(list0, list1);
149      if (b) {
150        matchedNames = new ArrayList<>(list1);
151      }
152      return b;
153    }
154
155    @Override protected void matched(List<String> prefixNames,
156        List<String> names) {
157      matchedNames =
158          new ArrayList<>(
159              startsWith(names, prefixNames)
160                  ? skip(names, prefixNames.size())
161                  : names);
162    }
163
164    @Override public List<String> bestMatch() {
165      return requireNonNull(matchedNames, "matchedNames");
166    }
167  }
168
169  // Utility methods
170
171  /** Returns whether one list is a prefix of another. */
172  private static <E> boolean startsWith(List<E> list0, List<E> list1) {
173    if (list0 == list1) {
174      return true;
175    }
176    final int size = list1.size();
177    if (list0.size() < size) {
178      return false;
179    }
180    for (int i = 0; i < size; i++) {
181      if (!java.util.Objects.equals(list0.get(i), list1.get(i))) {
182        return false;
183      }
184    }
185    return true;
186  }
187
188  /** Returns all but the first {@code n} elements of a list. */
189  private static <E> List<E> skip(List<E> list, int fromIndex) {
190    return fromIndex == 0 ? list : list.subList(fromIndex, list.size());
191  }
192
193  /** Converts a list to a dotted identifier string. */
194  private static String listToString(List<String> names) {
195    final int max = names.size() - 1;
196    switch (max) {
197    case -1:
198      return "";
199    case 0:
200      return String.valueOf(names.get(0));
201    default:
202      final StringBuilder buf = new StringBuilder();
203      for (int i = 0;; i++) {
204        buf.append(names.get(i));
205        if (i == max) {
206          return buf.toString();
207        }
208        buf.append(".");
209      }
210    }
211  }
212}