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.calcite.sqlnamematcher;
018
019import org.checkerframework.checker.nullness.qual.Nullable;
020
021import java.util.ArrayList;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.TreeSet;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Helpers for {@link SqlNameMatcher}.
032 *
033 * <p>This is a standalone version that can be used independently of Apache Calcite.
034 */
035public class SqlNameMatchers {
036
037  private static final BaseMatcher CASE_SENSITIVE = new BaseMatcher(true);
038  private static final BaseMatcher CASE_INSENSITIVE = new BaseMatcher(false);
039
040  private SqlNameMatchers() {}
041
042  /** Returns a name matcher with the given case sensitivity. */
043  public static SqlNameMatcher withCaseSensitive(final boolean caseSensitive) {
044    return caseSensitive ? CASE_SENSITIVE : CASE_INSENSITIVE;
045  }
046
047  /** Creates a name matcher that can suggest corrections to what the user
048   * typed. It matches liberally (case-insensitively) and also records the last
049   * match. */
050  public static SqlNameMatcher liberal() {
051    return new LiberalNameMatcher();
052  }
053
054  /** Partial implementation of {@link SqlNameMatcher}. */
055  private static class BaseMatcher implements SqlNameMatcher {
056    private final boolean caseSensitive;
057
058    BaseMatcher(boolean caseSensitive) {
059      this.caseSensitive = caseSensitive;
060    }
061
062    @Override public boolean isCaseSensitive() {
063      return caseSensitive;
064    }
065
066    @Override public boolean matches(String string, String name) {
067      return caseSensitive ? string.equals(name)
068          : string.equalsIgnoreCase(name);
069    }
070
071    protected boolean listMatches(List<String> list0, List<String> list1) {
072      if (list0.size() != list1.size()) {
073        return false;
074      }
075      for (int i = 0; i < list0.size(); i++) {
076        String s0 = list0.get(i);
077        String s1 = list1.get(i);
078        if (!matches(s0, s1)) {
079          return false;
080        }
081      }
082      return true;
083    }
084
085    @Override public <K extends List<String>, V> @Nullable V get(Map<K, V> map,
086        List<String> prefixNames, List<String> names) {
087      final List<String> key = concat(prefixNames, names);
088      if (caseSensitive) {
089        //noinspection SuspiciousMethodCalls
090        return map.get(key);
091      }
092      for (Map.Entry<K, V> entry : map.entrySet()) {
093        if (listMatches(key, entry.getKey())) {
094          matched(prefixNames, entry.getKey());
095          return entry.getValue();
096        }
097      }
098      return null;
099    }
100
101    private static List<String> concat(List<String> prefixNames, List<String> names) {
102      if (prefixNames.isEmpty()) {
103        return names;
104      } else {
105        List<String> result = new ArrayList<>(prefixNames.size() + names.size());
106        result.addAll(prefixNames);
107        result.addAll(names);
108        return result;
109      }
110    }
111
112    protected void matched(List<String> prefixNames, List<String> names) {
113    }
114
115    protected List<String> bestMatch() {
116      throw new UnsupportedOperationException();
117    }
118
119    @Override public String bestString() {
120      return listToString(bestMatch());
121    }
122
123    @Override public int frequency(Iterable<String> names, String name) {
124      int n = 0;
125      for (String s : names) {
126        if (matches(s, name)) {
127          ++n;
128        }
129      }
130      return n;
131    }
132
133    @Override public Set<String> createSet() {
134      return isCaseSensitive()
135          ? new LinkedHashSet<>()
136          : new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
137    }
138  }
139
140  /** Matcher that remembers the requests that were made of it. */
141  private static class LiberalNameMatcher extends BaseMatcher {
142    @Nullable List<String> matchedNames;
143
144    LiberalNameMatcher() {
145      super(false);
146    }
147
148    @Override protected boolean listMatches(List<String> list0,
149        List<String> list1) {
150      final boolean b = super.listMatches(list0, list1);
151      if (b) {
152        matchedNames = new ArrayList<>(list1);
153      }
154      return b;
155    }
156
157    @Override protected void matched(List<String> prefixNames,
158        List<String> names) {
159      matchedNames =
160          new ArrayList<>(
161              startsWith(names, prefixNames)
162                  ? skip(names, prefixNames.size())
163                  : names);
164    }
165
166    @Override public List<String> bestMatch() {
167      return requireNonNull(matchedNames, "matchedNames");
168    }
169  }
170
171  // Utility methods
172
173  /** Returns whether one list is a prefix of another. */
174  private static <E> boolean startsWith(List<E> list0, List<E> list1) {
175    if (list0 == list1) {
176      return true;
177    }
178    final int size = list1.size();
179    if (list0.size() < size) {
180      return false;
181    }
182    for (int i = 0; i < size; i++) {
183      if (!java.util.Objects.equals(list0.get(i), list1.get(i))) {
184        return false;
185      }
186    }
187    return true;
188  }
189
190  /** Returns all but the first {@code n} elements of a list. */
191  private static <E> List<E> skip(List<E> list, int fromIndex) {
192    return fromIndex == 0 ? list : list.subList(fromIndex, list.size());
193  }
194
195  /** Converts a list to a dotted identifier string. */
196  private static String listToString(List<String> names) {
197    final int max = names.size() - 1;
198    switch (max) {
199    case -1:
200      return "";
201    case 0:
202      return String.valueOf(names.get(0));
203    default:
204      final StringBuilder buf = new StringBuilder();
205      for (int i = 0;; i++) {
206        buf.append(names.get(i));
207        if (i == max) {
208          return buf.toString();
209        }
210        buf.append(".");
211      }
212    }
213  }
214}