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}