|
19 | 19 | import java.util.ArrayList; |
20 | 20 | import java.util.Iterator; |
21 | 21 | import java.util.List; |
| 22 | +import java.util.Locale; |
22 | 23 | import java.util.stream.Collectors; |
23 | 24 |
|
24 | 25 | public class ChestDatabase |
@@ -180,60 +181,115 @@ public synchronized void removeAt(String serverIp, String dimension, int x, |
180 | 181 |
|
181 | 182 | public synchronized List<ChestEntry> search(String query) |
182 | 183 | { |
183 | | - String q = query.toLowerCase(); |
| 184 | + String q = query == null ? "" : query.toLowerCase(Locale.ROOT).trim(); |
| 185 | + String[] tokens = tokenizeQuery(q); |
184 | 186 | List<ChestEntry> res = new ArrayList<>(); |
185 | 187 | for(ChestEntry e : entries) |
186 | 188 | { |
187 | 189 | boolean matched = false; |
188 | | - if(e.serverIp != null && e.serverIp.toLowerCase().contains(q)) |
| 190 | + if(e.serverIp != null && containsQueryTokens( |
| 191 | + e.serverIp.toLowerCase(Locale.ROOT), q, tokens)) |
189 | 192 | matched = true; |
190 | | - if(e.dimension != null && e.dimension.toLowerCase().contains(q)) |
| 193 | + if(e.dimension != null && containsQueryTokens( |
| 194 | + e.dimension.toLowerCase(Locale.ROOT), q, tokens)) |
191 | 195 | matched = true; |
192 | | - for(ChestEntry.ItemEntry item : e.items) |
| 196 | + if(!matched && e.items != null) |
193 | 197 | { |
194 | | - if(item.itemId != null && item.itemId.toLowerCase().contains(q)) |
195 | | - matched = true; |
196 | | - if(item.displayName != null |
197 | | - && item.displayName.toLowerCase().contains(q)) |
198 | | - matched = true; |
199 | | - if(!matched && item.nbt != null |
200 | | - && item.nbt.toString().toLowerCase().contains(q)) |
201 | | - matched = true; |
202 | | - // Also match extracted enchantment and potion ids collected |
203 | | - // by the recorder so searches like "sharpness" or "speed" |
204 | | - // will find chests containing those effects. |
205 | | - if(!matched && item.enchantments != null) |
| 198 | + for(ChestEntry.ItemEntry item : e.items) |
206 | 199 | { |
207 | | - for(String en : item.enchantments) |
| 200 | + if(itemMatchesQuery(item, q, tokens)) |
208 | 201 | { |
209 | | - if(en != null && en.toLowerCase().contains(q)) |
210 | | - { |
211 | | - matched = true; |
212 | | - break; |
213 | | - } |
| 202 | + matched = true; |
| 203 | + break; |
214 | 204 | } |
215 | 205 | } |
216 | | - if(!matched && item.potionEffects != null) |
217 | | - { |
218 | | - for(String pe : item.potionEffects) |
219 | | - { |
220 | | - if(pe != null && pe.toLowerCase().contains(q)) |
221 | | - { |
222 | | - matched = true; |
223 | | - break; |
224 | | - } |
225 | | - } |
226 | | - } |
227 | | - if(!matched && item.primaryPotion != null |
228 | | - && item.primaryPotion.toLowerCase().contains(q)) |
229 | | - matched = true; |
230 | 206 | } |
231 | 207 | if(matched) |
232 | 208 | res.add(e); |
233 | 209 | } |
234 | 210 | return res; |
235 | 211 | } |
236 | 212 |
|
| 213 | + private static boolean itemMatchesQuery(ChestEntry.ItemEntry item, String q, |
| 214 | + String[] tokens) |
| 215 | + { |
| 216 | + if(item == null) |
| 217 | + return false; |
| 218 | + if(q.isEmpty()) |
| 219 | + return true; |
| 220 | + |
| 221 | + StringBuilder sb = new StringBuilder(256); |
| 222 | + appendSearchPart(sb, item.itemId); |
| 223 | + appendSearchPart(sb, item.displayName); |
| 224 | + if(item.nbt != null) |
| 225 | + appendSearchPart(sb, item.nbt.toString()); |
| 226 | + if(item.enchantments != null) |
| 227 | + for(String en : item.enchantments) |
| 228 | + appendSearchPart(sb, en); |
| 229 | + if(item.potionEffects != null) |
| 230 | + for(String pe : item.potionEffects) |
| 231 | + appendSearchPart(sb, pe); |
| 232 | + appendSearchPart(sb, item.primaryPotion); |
| 233 | + |
| 234 | + return containsQueryTokens(sb.toString(), q, tokens); |
| 235 | + } |
| 236 | + |
| 237 | + private static void appendSearchPart(StringBuilder sb, String part) |
| 238 | + { |
| 239 | + if(part == null || part.isBlank()) |
| 240 | + return; |
| 241 | + if(sb.length() > 0) |
| 242 | + sb.append(' '); |
| 243 | + sb.append(part.toLowerCase(Locale.ROOT)); |
| 244 | + } |
| 245 | + |
| 246 | + private static String[] tokenizeQuery(String query) |
| 247 | + { |
| 248 | + if(query == null || query.isBlank()) |
| 249 | + return new String[0]; |
| 250 | + String normalized = normalizeForTokenSearch(query); |
| 251 | + if(normalized.isEmpty()) |
| 252 | + return new String[0]; |
| 253 | + return normalized.split(" "); |
| 254 | + } |
| 255 | + |
| 256 | + private static boolean containsQueryTokens(String haystack, String rawQuery, |
| 257 | + String[] tokens) |
| 258 | + { |
| 259 | + if(rawQuery == null || rawQuery.isEmpty()) |
| 260 | + return true; |
| 261 | + if(haystack == null || haystack.isEmpty()) |
| 262 | + return false; |
| 263 | + |
| 264 | + String lowerHaystack = haystack.toLowerCase(Locale.ROOT); |
| 265 | + if(lowerHaystack.contains(rawQuery)) |
| 266 | + return true; |
| 267 | + |
| 268 | + if(tokens == null || tokens.length == 0) |
| 269 | + return false; |
| 270 | + |
| 271 | + String normalizedHaystack = normalizeForTokenSearch(lowerHaystack); |
| 272 | + for(String token : tokens) |
| 273 | + { |
| 274 | + if(token == null || token.isEmpty()) |
| 275 | + continue; |
| 276 | + if(!lowerHaystack.contains(token) |
| 277 | + && !normalizedHaystack.contains(token)) |
| 278 | + { |
| 279 | + return false; |
| 280 | + } |
| 281 | + } |
| 282 | + return true; |
| 283 | + } |
| 284 | + |
| 285 | + private static String normalizeForTokenSearch(String value) |
| 286 | + { |
| 287 | + if(value == null || value.isBlank()) |
| 288 | + return ""; |
| 289 | + return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", " ") |
| 290 | + .trim(); |
| 291 | + } |
| 292 | + |
237 | 293 | private boolean equalsPos(ChestEntry a, ChestEntry b) |
238 | 294 | { |
239 | 295 | if(a == null || b == null) |
|
0 commit comments