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}