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.sqlenv; 018 019import gudusoft.gsqlparser.EDbVendor; 020import gudusoft.gsqlparser.ext.sqlnamematcher.SqlNameMatcher; 021 022import java.util.*; 023 024/** 025 * Centralized name matching service using SqlNameMatcher framework. 026 * 027 * <p>This service provides vendor-aware and type-aware name comparison utilities, 028 * replacing scattered string comparisons throughout GSP with a unified approach.</p> 029 * 030 * <p>Key responsibilities:</p> 031 * <ul> 032 * <li>Name equality checking (case-sensitive or insensitive per vendor/type)</li> 033 * <li>Case-aware collection operations (indexOf, distinct, Set creation)</li> 034 * <li>Bridge between GSP's normalization rules and the matching framework</li> 035 * </ul> 036 * 037 * <p>Design: This service delegates to {@link NamePolicyFactory} for matcher instances 038 * and preserves {@link TSQLEnv#normalizeIdentifier} as the authority for identifier 039 * quoting and default-case normalization.</p> 040 * 041 * @since 3.2.0.3 (Phase 1) 042 */ 043public class NameService { 044 045 private final EDbVendor vendor; 046 private final NamePolicyFactory factory; 047 048 /** 049 * Creates a new NameService for the specified database vendor. 050 * 051 * @param vendor the database vendor 052 */ 053 public NameService(EDbVendor vendor) { 054 if (vendor == null) { 055 throw new IllegalArgumentException("vendor cannot be null"); 056 } 057 this.vendor = vendor; 058 this.factory = new NamePolicyFactory(vendor); 059 } 060 061 /** 062 * Returns the database vendor associated with this service. 063 * 064 * @return the database vendor 065 */ 066 public EDbVendor getVendor() { 067 return vendor; 068 } 069 070 /** 071 * Checks if two names are equal according to vendor-specific and type-specific rules. 072 * 073 * <p>This method replaces {@code TSQLEnv.compareIdentifier()} with a more robust 074 * implementation backed by SqlNameMatcher framework.</p> 075 * 076 * <p>Names are normalized using GSP's existing rules before matching.</p> 077 * 078 * @param type the SQL object type (table, column, function, etc.) 079 * @param name1 the first name to compare 080 * @param name2 the second name to compare 081 * @return true if the names match according to vendor/type rules 082 */ 083 public boolean equals(ESQLDataObjectType type, String name1, String name2) { 084 if (name1 == null || name2 == null) { 085 return name1 == name2; 086 } 087 088 // Normalize identifiers using GSP's existing quoting/case rules 089 String normalized1 = IdentifierService.normalizeStatic(vendor, type, name1); 090 String normalized2 = IdentifierService.normalizeStatic(vendor, type, name2); 091 092 // Use matcher for case-aware comparison 093 SqlNameMatcher matcher = factory.getMatcherForType(type); 094 return matcher.matches(normalized1, normalized2); 095 } 096 097 /** 098 * Finds the index of a name in a collection, using vendor/type-aware matching. 099 * 100 * <p>This is a case-aware version of {@code List.indexOf()}.</p> 101 * 102 * @param type the SQL object type 103 * @param names the collection of names to search 104 * @param target the name to find 105 * @return the index of the first matching name, or -1 if not found 106 */ 107 public int indexOf(ESQLDataObjectType type, Iterable<String> names, String target) { 108 if (names == null || target == null) { 109 return -1; 110 } 111 112 int index = 0; 113 for (String name : names) { 114 if (equals(type, name, target)) { 115 return index; 116 } 117 index++; 118 } 119 return -1; 120 } 121 122 /** 123 * Creates a distinct copy of a collection, removing duplicates using vendor/type-aware matching. 124 * 125 * <p>Preserves insertion order. For example, with case-insensitive matching:</p> 126 * <pre> 127 * ["foo", "FOO", "bar", "Bar"] → ["foo", "bar"] 128 * </pre> 129 * 130 * @param type the SQL object type 131 * @param names the collection of names (may contain duplicates) 132 * @return a new list with duplicates removed, preserving order 133 */ 134 public List<String> distinctCopy(ESQLDataObjectType type, Iterable<String> names) { 135 if (names == null) { 136 return new ArrayList<>(); 137 } 138 139 Set<String> seen = createSet(type); 140 List<String> result = new ArrayList<>(); 141 142 for (String name : names) { 143 if (name != null) { 144 String normalized = IdentifierService.normalizeStatic(vendor, type, name); 145 if (seen.add(normalized)) { 146 result.add(name); // Keep original name, not normalized 147 } 148 } 149 } 150 151 return result; 152 } 153 154 /** 155 * Creates a Set that uses vendor/type-aware name matching for equality. 156 * 157 * <p>The returned Set will treat names as equal if they match according to 158 * the vendor's case sensitivity rules for the specified object type.</p> 159 * 160 * <p>Implementation note: Uses a wrapper around LinkedHashSet that normalizes 161 * keys before insertion/lookup.</p> 162 * 163 * @param type the SQL object type 164 * @return a new Set with case-aware equality 165 */ 166 public Set<String> createSet(ESQLDataObjectType type) { 167 return new NameSet(type); 168 } 169 170 /** 171 * Checks if a specific object type is case-sensitive for this vendor. 172 * 173 * <p>Convenience method that delegates to {@link NamePolicyFactory}.</p> 174 * 175 * @param type the object type 176 * @return true if case-sensitive, false otherwise 177 */ 178 public boolean isCaseSensitive(ESQLDataObjectType type) { 179 return factory.isCaseSensitive(type); 180 } 181 182 /** 183 * Internal Set implementation that uses vendor/type-aware name matching. 184 * 185 * <p>This wrapper normalizes all keys before delegating to a standard LinkedHashSet, 186 * ensuring that name matching follows vendor-specific case sensitivity rules.</p> 187 */ 188 private class NameSet extends AbstractSet<String> { 189 private final ESQLDataObjectType type; 190 private final Map<String, String> backingMap; // normalized -> original 191 192 NameSet(ESQLDataObjectType type) { 193 this.type = type; 194 this.backingMap = new LinkedHashMap<>(); 195 } 196 197 @Override 198 public boolean add(String name) { 199 if (name == null) { 200 throw new NullPointerException("NameSet does not permit null elements"); 201 } 202 String normalized = IdentifierService.normalizeStatic(vendor, type, name); 203 return backingMap.putIfAbsent(normalized, name) == null; 204 } 205 206 @Override 207 public boolean contains(Object o) { 208 if (!(o instanceof String)) { 209 return false; 210 } 211 String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o); 212 return backingMap.containsKey(normalized); 213 } 214 215 @Override 216 public boolean remove(Object o) { 217 if (!(o instanceof String)) { 218 return false; 219 } 220 String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o); 221 return backingMap.remove(normalized) != null; 222 } 223 224 @Override 225 public Iterator<String> iterator() { 226 return backingMap.values().iterator(); 227 } 228 229 @Override 230 public int size() { 231 return backingMap.size(); 232 } 233 234 @Override 235 public void clear() { 236 backingMap.clear(); 237 } 238 } 239}