summaryrefslogtreecommitdiff
path: root/src/main/java/cuchaz/enigma/utils
diff options
context:
space:
mode:
authorGravatar Runemoro2020-06-03 13:39:42 -0400
committerGravatar GitHub2020-06-03 18:39:42 +0100
commit0f47403d0220757fed189b76e2071e25b1025cb8 (patch)
tree879bf72c4476f0a5e0d82da99d7ff2b2276bcaca /src/main/java/cuchaz/enigma/utils
parentFix search dialog hanging for a short time sometimes (#250) (diff)
downloadenigma-fork-0f47403d0220757fed189b76e2071e25b1025cb8.tar.gz
enigma-fork-0f47403d0220757fed189b76e2071e25b1025cb8.tar.xz
enigma-fork-0f47403d0220757fed189b76e2071e25b1025cb8.zip
Split GUI code to separate module (#242)
* Split into modules * Post merge compile fixes Co-authored-by: modmuss50 <modmuss50@gmail.com>
Diffstat (limited to 'src/main/java/cuchaz/enigma/utils')
-rw-r--r--src/main/java/cuchaz/enigma/utils/I18n.java102
-rw-r--r--src/main/java/cuchaz/enigma/utils/LFPrintWriter.java16
-rw-r--r--src/main/java/cuchaz/enigma/utils/Message.java392
-rw-r--r--src/main/java/cuchaz/enigma/utils/Pair.java26
-rw-r--r--src/main/java/cuchaz/enigma/utils/ReadableToken.java30
-rw-r--r--src/main/java/cuchaz/enigma/utils/Utils.java179
-rw-r--r--src/main/java/cuchaz/enigma/utils/search/SearchEntry.java17
-rw-r--r--src/main/java/cuchaz/enigma/utils/search/SearchUtil.java268
8 files changed, 0 insertions, 1030 deletions
diff --git a/src/main/java/cuchaz/enigma/utils/I18n.java b/src/main/java/cuchaz/enigma/utils/I18n.java
deleted file mode 100644
index f91c916..0000000
--- a/src/main/java/cuchaz/enigma/utils/I18n.java
+++ /dev/null
@@ -1,102 +0,0 @@
1package cuchaz.enigma.utils;
2
3import java.io.BufferedReader;
4import java.io.IOException;
5import java.io.InputStream;
6import java.io.InputStreamReader;
7import java.nio.charset.StandardCharsets;
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.Map;
11import java.util.stream.Stream;
12
13import com.google.common.collect.ImmutableList;
14import com.google.common.collect.Maps;
15import com.google.common.reflect.ClassPath;
16import com.google.common.reflect.ClassPath.ResourceInfo;
17import com.google.gson.Gson;
18
19import cuchaz.enigma.config.Config;
20
21public class I18n {
22 public static final String DEFAULT_LANGUAGE = "en_us";
23 private static final Gson GSON = new Gson();
24 private static Map<String, String> translations = Maps.newHashMap();
25 private static Map<String, String> defaultTranslations = Maps.newHashMap();
26 private static Map<String, String> languageNames = Maps.newHashMap();
27
28 static {
29 translations = load(Config.getInstance().language);
30 defaultTranslations = load(DEFAULT_LANGUAGE);
31 }
32
33 @SuppressWarnings("unchecked")
34 public static Map<String, String> load(String language) {
35 try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) {
36 if (inputStream != null) {
37 try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
38 return GSON.fromJson(reader, Map.class);
39 }
40 }
41 } catch (IOException e) {
42 e.printStackTrace();
43 }
44 return Collections.emptyMap();
45 }
46
47 public static String translate(String key) {
48 String value = translations.get(key);
49 if (value != null) {
50 return value;
51 }
52 value = defaultTranslations.get(key);
53 if (value != null) {
54 return value;
55 }
56 return key;
57 }
58
59 public static String getLanguageName(String language) {
60 return languageNames.get(language);
61 }
62
63 public static void setLanguage(String language) {
64 Config.getInstance().language = language;
65 try {
66 Config.getInstance().saveConfig();
67 } catch (IOException e) {
68 e.printStackTrace();
69 }
70 }
71
72 public static ArrayList<String> getAvailableLanguages() {
73 ArrayList<String> list = new ArrayList<String>();
74
75 try {
76 ImmutableList<ResourceInfo> resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList();
77 Stream<ResourceInfo> dirStream = resources.stream();
78 dirStream.forEach(context -> {
79 String file = context.getResourceName();
80 if (file.startsWith("lang/") && file.endsWith(".json")) {
81 String fileName = file.substring(5, file.length() - 5);
82 list.add(fileName);
83 loadLanguageName(fileName);
84 }
85 });
86 } catch (IOException e) {
87 e.printStackTrace();
88 }
89 return list;
90 }
91
92 private static void loadLanguageName(String fileName) {
93 try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) {
94 try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
95 Map<?, ?> map = GSON.fromJson(reader, Map.class);
96 languageNames.put(fileName, map.get("language").toString());
97 }
98 } catch (IOException e) {
99 e.printStackTrace();
100 }
101 }
102}
diff --git a/src/main/java/cuchaz/enigma/utils/LFPrintWriter.java b/src/main/java/cuchaz/enigma/utils/LFPrintWriter.java
deleted file mode 100644
index c12e913..0000000
--- a/src/main/java/cuchaz/enigma/utils/LFPrintWriter.java
+++ /dev/null
@@ -1,16 +0,0 @@
1package cuchaz.enigma.utils;
2
3import java.io.PrintWriter;
4import java.io.Writer;
5
6public class LFPrintWriter extends PrintWriter {
7 public LFPrintWriter(Writer out) {
8 super(out);
9 }
10
11 @Override
12 public void println() {
13 // https://stackoverflow.com/a/14749004
14 write('\n');
15 }
16}
diff --git a/src/main/java/cuchaz/enigma/utils/Message.java b/src/main/java/cuchaz/enigma/utils/Message.java
deleted file mode 100644
index d7c5f23..0000000
--- a/src/main/java/cuchaz/enigma/utils/Message.java
+++ /dev/null
@@ -1,392 +0,0 @@
1package cuchaz.enigma.utils;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6import java.util.Objects;
7
8import cuchaz.enigma.network.packet.PacketHelper;
9import cuchaz.enigma.translation.representation.entry.Entry;
10
11public abstract class Message {
12
13 public final String user;
14
15 public static Chat chat(String user, String message) {
16 return new Chat(user, message);
17 }
18
19 public static Connect connect(String user) {
20 return new Connect(user);
21 }
22
23 public static Disconnect disconnect(String user) {
24 return new Disconnect(user);
25 }
26
27 public static EditDocs editDocs(String user, Entry<?> entry) {
28 return new EditDocs(user, entry);
29 }
30
31 public static MarkDeobf markDeobf(String user, Entry<?> entry) {
32 return new MarkDeobf(user, entry);
33 }
34
35 public static RemoveMapping removeMapping(String user, Entry<?> entry) {
36 return new RemoveMapping(user, entry);
37 }
38
39 public static Rename rename(String user, Entry<?> entry, String newName) {
40 return new Rename(user, entry, newName);
41 }
42
43 public abstract String translate();
44
45 public abstract Type getType();
46
47 public static Message read(DataInput input) throws IOException {
48 byte typeId = input.readByte();
49 if (typeId < 0 || typeId >= Type.values().length) {
50 throw new IOException(String.format("Invalid message type ID %d", typeId));
51 }
52 Type type = Type.values()[typeId];
53 String user = input.readUTF();
54 switch (type) {
55 case CHAT:
56 String message = input.readUTF();
57 return chat(user, message);
58 case CONNECT:
59 return connect(user);
60 case DISCONNECT:
61 return disconnect(user);
62 case EDIT_DOCS:
63 Entry<?> entry = PacketHelper.readEntry(input);
64 return editDocs(user, entry);
65 case MARK_DEOBF:
66 entry = PacketHelper.readEntry(input);
67 return markDeobf(user, entry);
68 case REMOVE_MAPPING:
69 entry = PacketHelper.readEntry(input);
70 return removeMapping(user, entry);
71 case RENAME:
72 entry = PacketHelper.readEntry(input);
73 String newName = input.readUTF();
74 return rename(user, entry, newName);
75 default:
76 throw new IllegalStateException("unreachable");
77 }
78 }
79
80 public void write(DataOutput output) throws IOException {
81 output.writeByte(getType().ordinal());
82 PacketHelper.writeString(output, user);
83 }
84
85 private Message(String user) {
86 this.user = user;
87 }
88
89 @Override
90 public boolean equals(Object o) {
91 if (this == o) return true;
92 if (o == null || getClass() != o.getClass()) return false;
93 Message message = (Message) o;
94 return Objects.equals(user, message.user);
95 }
96
97 @Override
98 public int hashCode() {
99 return Objects.hash(user);
100 }
101
102 public enum Type {
103 CHAT,
104 CONNECT,
105 DISCONNECT,
106 EDIT_DOCS,
107 MARK_DEOBF,
108 REMOVE_MAPPING,
109 RENAME,
110 }
111
112 public static final class Chat extends Message {
113
114 public final String message;
115
116 private Chat(String user, String message) {
117 super(user);
118 this.message = message;
119 }
120
121 @Override
122 public void write(DataOutput output) throws IOException {
123 super.write(output);
124 PacketHelper.writeString(output, message);
125 }
126
127 @Override
128 public String translate() {
129 return String.format(I18n.translate("message.chat.text"), user, message);
130 }
131
132 @Override
133 public Type getType() {
134 return Type.CHAT;
135 }
136
137 @Override
138 public boolean equals(Object o) {
139 if (this == o) return true;
140 if (o == null || getClass() != o.getClass()) return false;
141 if (!super.equals(o)) return false;
142 Chat chat = (Chat) o;
143 return Objects.equals(message, chat.message);
144 }
145
146 @Override
147 public int hashCode() {
148 return Objects.hash(super.hashCode(), message);
149 }
150
151 @Override
152 public String toString() {
153 return String.format("Message.Chat { user: '%s', message: '%s' }", user, message);
154 }
155
156 }
157
158 public static final class Connect extends Message {
159
160 private Connect(String user) {
161 super(user);
162 }
163
164 @Override
165 public String translate() {
166 return String.format(I18n.translate("message.connect.text"), user);
167 }
168
169 @Override
170 public Type getType() {
171 return Type.CONNECT;
172 }
173
174 @Override
175 public String toString() {
176 return String.format("Message.Connect { user: '%s' }", user);
177 }
178
179 }
180
181 public static final class Disconnect extends Message {
182
183 private Disconnect(String user) {
184 super(user);
185 }
186
187 @Override
188 public String translate() {
189 return String.format(I18n.translate("message.disconnect.text"), user);
190 }
191
192 @Override
193 public Type getType() {
194 return Type.DISCONNECT;
195 }
196
197 @Override
198 public String toString() {
199 return String.format("Message.Disconnect { user: '%s' }", user);
200 }
201
202 }
203
204 public static final class EditDocs extends Message {
205
206 public final Entry<?> entry;
207
208 private EditDocs(String user, Entry<?> entry) {
209 super(user);
210 this.entry = entry;
211 }
212
213 @Override
214 public void write(DataOutput output) throws IOException {
215 super.write(output);
216 PacketHelper.writeEntry(output, entry);
217 }
218
219 @Override
220 public String translate() {
221 return String.format(I18n.translate("message.edit_docs.text"), user, entry);
222 }
223
224 @Override
225 public Type getType() {
226 return Type.EDIT_DOCS;
227 }
228
229 @Override
230 public boolean equals(Object o) {
231 if (this == o) return true;
232 if (o == null || getClass() != o.getClass()) return false;
233 if (!super.equals(o)) return false;
234 EditDocs editDocs = (EditDocs) o;
235 return Objects.equals(entry, editDocs.entry);
236 }
237
238 @Override
239 public int hashCode() {
240 return Objects.hash(super.hashCode(), entry);
241 }
242
243 @Override
244 public String toString() {
245 return String.format("Message.EditDocs { user: '%s', entry: %s }", user, entry);
246 }
247
248 }
249
250 public static final class MarkDeobf extends Message {
251
252 public final Entry<?> entry;
253
254 private MarkDeobf(String user, Entry<?> entry) {
255 super(user);
256 this.entry = entry;
257 }
258
259 @Override
260 public void write(DataOutput output) throws IOException {
261 super.write(output);
262 PacketHelper.writeEntry(output, entry);
263 }
264
265 @Override
266 public String translate() {
267 return String.format(I18n.translate("message.mark_deobf.text"), user, entry);
268 }
269
270 @Override
271 public Type getType() {
272 return Type.MARK_DEOBF;
273 }
274
275 @Override
276 public boolean equals(Object o) {
277 if (this == o) return true;
278 if (o == null || getClass() != o.getClass()) return false;
279 if (!super.equals(o)) return false;
280 MarkDeobf markDeobf = (MarkDeobf) o;
281 return Objects.equals(entry, markDeobf.entry);
282 }
283
284 @Override
285 public int hashCode() {
286 return Objects.hash(super.hashCode(), entry);
287 }
288
289 @Override
290 public String toString() {
291 return String.format("Message.MarkDeobf { user: '%s', entry: %s }", user, entry);
292 }
293
294 }
295
296 public static final class RemoveMapping extends Message {
297
298 public final Entry<?> entry;
299
300 private RemoveMapping(String user, Entry<?> entry) {
301 super(user);
302 this.entry = entry;
303 }
304
305 @Override
306 public void write(DataOutput output) throws IOException {
307 super.write(output);
308 PacketHelper.writeEntry(output, entry);
309 }
310
311 @Override
312 public String translate() {
313 return String.format(I18n.translate("message.remove_mapping.text"), user, entry);
314 }
315
316 @Override
317 public Type getType() {
318 return Type.REMOVE_MAPPING;
319 }
320
321 @Override
322 public boolean equals(Object o) {
323 if (this == o) return true;
324 if (o == null || getClass() != o.getClass()) return false;
325 if (!super.equals(o)) return false;
326 RemoveMapping that = (RemoveMapping) o;
327 return Objects.equals(entry, that.entry);
328 }
329
330 @Override
331 public int hashCode() {
332 return Objects.hash(super.hashCode(), entry);
333 }
334
335 @Override
336 public String toString() {
337 return String.format("Message.RemoveMapping { user: '%s', entry: %s }", user, entry);
338 }
339
340 }
341
342 public static final class Rename extends Message {
343
344 public final Entry<?> entry;
345 public final String newName;
346
347 private Rename(String user, Entry<?> entry, String newName) {
348 super(user);
349 this.entry = entry;
350 this.newName = newName;
351 }
352
353 @Override
354 public void write(DataOutput output) throws IOException {
355 super.write(output);
356 PacketHelper.writeEntry(output, entry);
357 PacketHelper.writeString(output, newName);
358 }
359
360 @Override
361 public String translate() {
362 return String.format(I18n.translate("message.rename.text"), user, entry, newName);
363 }
364
365 @Override
366 public Type getType() {
367 return Type.RENAME;
368 }
369
370 @Override
371 public boolean equals(Object o) {
372 if (this == o) return true;
373 if (o == null || getClass() != o.getClass()) return false;
374 if (!super.equals(o)) return false;
375 Rename rename = (Rename) o;
376 return Objects.equals(entry, rename.entry) &&
377 Objects.equals(newName, rename.newName);
378 }
379
380 @Override
381 public int hashCode() {
382 return Objects.hash(super.hashCode(), entry, newName);
383 }
384
385 @Override
386 public String toString() {
387 return String.format("Message.Rename { user: '%s', entry: %s, newName: '%s' }", user, entry, newName);
388 }
389
390 }
391
392}
diff --git a/src/main/java/cuchaz/enigma/utils/Pair.java b/src/main/java/cuchaz/enigma/utils/Pair.java
deleted file mode 100644
index bf02cef..0000000
--- a/src/main/java/cuchaz/enigma/utils/Pair.java
+++ /dev/null
@@ -1,26 +0,0 @@
1package cuchaz.enigma.utils;
2
3import java.util.Objects;
4
5public class Pair<A, B> {
6 public final A a;
7 public final B b;
8
9 public Pair(A a, B b) {
10 this.a = a;
11 this.b = b;
12 }
13
14 @Override
15 public int hashCode() {
16 return Objects.hashCode(a) * 31 +
17 Objects.hashCode(b);
18 }
19
20 @Override
21 public boolean equals(Object o) {
22 return o instanceof Pair &&
23 Objects.equals(a, ((Pair<?, ?>) o).a) &&
24 Objects.equals(b, ((Pair<?, ?>) o).b);
25 }
26}
diff --git a/src/main/java/cuchaz/enigma/utils/ReadableToken.java b/src/main/java/cuchaz/enigma/utils/ReadableToken.java
deleted file mode 100644
index de152fe..0000000
--- a/src/main/java/cuchaz/enigma/utils/ReadableToken.java
+++ /dev/null
@@ -1,30 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.utils;
13
14public class ReadableToken {
15
16 public int line;
17 public int startColumn;
18 public int endColumn;
19
20 public ReadableToken(int line, int startColumn, int endColumn) {
21 this.line = line;
22 this.startColumn = startColumn;
23 this.endColumn = endColumn;
24 }
25
26 @Override
27 public String toString() {
28 return "line " + line + " columns " + startColumn + "-" + endColumn;
29 }
30}
diff --git a/src/main/java/cuchaz/enigma/utils/Utils.java b/src/main/java/cuchaz/enigma/utils/Utils.java
deleted file mode 100644
index b45b00d..0000000
--- a/src/main/java/cuchaz/enigma/utils/Utils.java
+++ /dev/null
@@ -1,179 +0,0 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11
12package cuchaz.enigma.utils;
13
14import com.google.common.io.CharStreams;
15import org.objectweb.asm.Opcodes;
16
17import javax.swing.*;
18import javax.swing.text.BadLocationException;
19import javax.swing.text.JTextComponent;
20import java.awt.*;
21import java.awt.event.MouseEvent;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.InputStreamReader;
25import java.net.URI;
26import java.net.URISyntaxException;
27import java.nio.charset.StandardCharsets;
28import java.nio.file.Files;
29import java.nio.file.Path;
30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.util.*;
33import java.util.List;
34import java.util.stream.Collectors;
35import java.util.zip.ZipEntry;
36import java.util.zip.ZipFile;
37
38public class Utils {
39
40 public static final int ASM_VERSION = Opcodes.ASM8;
41
42 public static int combineHashesOrdered(Object... objs) {
43 final int prime = 67;
44 int result = 1;
45 for (Object obj : objs) {
46 result *= prime;
47 if (obj != null) {
48 result += obj.hashCode();
49 }
50 }
51 return result;
52 }
53
54 public static int combineHashesOrdered(List<Object> objs) {
55 final int prime = 67;
56 int result = 1;
57 for (Object obj : objs) {
58 result *= prime;
59 if (obj != null) {
60 result += obj.hashCode();
61 }
62 }
63 return result;
64 }
65
66 public static String readStreamToString(InputStream in) throws IOException {
67 return CharStreams.toString(new InputStreamReader(in, "UTF-8"));
68 }
69
70 public static String readResourceToString(String path) throws IOException {
71 InputStream in = Utils.class.getResourceAsStream(path);
72 if (in == null) {
73 throw new IllegalArgumentException("Resource not found! " + path);
74 }
75 return readStreamToString(in);
76 }
77
78 public static void openUrl(String url) {
79 if (Desktop.isDesktopSupported()) {
80 Desktop desktop = Desktop.getDesktop();
81 try {
82 desktop.browse(new URI(url));
83 } catch (IOException ex) {
84 throw new Error(ex);
85 } catch (URISyntaxException ex) {
86 throw new IllegalArgumentException(ex);
87 }
88 }
89 }
90
91 public static JLabel unboldLabel(JLabel label) {
92 Font font = label.getFont();
93 label.setFont(font.deriveFont(font.getStyle() & ~Font.BOLD));
94 return label;
95 }
96
97 public static void showToolTipNow(JComponent component) {
98 // HACKHACK: trick the tooltip manager into showing the tooltip right now
99 ToolTipManager manager = ToolTipManager.sharedInstance();
100 int oldDelay = manager.getInitialDelay();
101 manager.setInitialDelay(0);
102 manager.mouseMoved(new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 0, 0, 0, false));
103 manager.setInitialDelay(oldDelay);
104 }
105
106 public static Rectangle safeModelToView(JTextComponent component, int modelPos) {
107 if (modelPos < 0) {
108 modelPos = 0;
109 } else if (modelPos >= component.getText().length()) {
110 modelPos = component.getText().length();
111 }
112 try {
113 return component.modelToView(modelPos);
114 } catch (BadLocationException e) {
115 throw new RuntimeException(e);
116 }
117 }
118
119 public static boolean getSystemPropertyAsBoolean(String property, boolean defValue) {
120 String value = System.getProperty(property);
121 return value == null ? defValue : Boolean.parseBoolean(value);
122 }
123
124 public static void delete(Path path) throws IOException {
125 if (Files.exists(path)) {
126 for (Path p : Files.walk(path).sorted(Comparator.reverseOrder()).collect(Collectors.toList())) {
127 Files.delete(p);
128 }
129 }
130 }
131
132 public static byte[] zipSha1(Path path) throws IOException {
133 MessageDigest digest;
134 try {
135 digest = MessageDigest.getInstance("SHA-1");
136 } catch (NoSuchAlgorithmException e) {
137 // Algorithm guaranteed to be supported
138 throw new RuntimeException(e);
139 }
140 try (ZipFile zip = new ZipFile(path.toFile())) {
141 List<? extends ZipEntry> entries = Collections.list(zip.entries());
142 // only compare classes (some implementations may not generate directory entries)
143 entries.removeIf(entry -> !entry.getName().toLowerCase(Locale.ROOT).endsWith(".class"));
144 // different implementations may add zip entries in a different order
145 entries.sort(Comparator.comparing(ZipEntry::getName));
146 byte[] buffer = new byte[8192];
147 for (ZipEntry entry : entries) {
148 digest.update(entry.getName().getBytes(StandardCharsets.UTF_8));
149 try (InputStream in = zip.getInputStream(entry)) {
150 int n;
151 while ((n = in.read(buffer)) != -1) {
152 digest.update(buffer, 0, n);
153 }
154 }
155 }
156 }
157 return digest.digest();
158 }
159
160 public static String caplisiseCamelCase(String input){
161 StringJoiner stringJoiner = new StringJoiner(" ");
162 for (String word : input.toLowerCase(Locale.ROOT).split("_")) {
163 stringJoiner.add(word.substring(0, 1).toUpperCase(Locale.ROOT) + word.substring(1));
164 }
165 return stringJoiner.toString();
166 }
167
168 public static boolean isBlank(String input) {
169 if (input == null) {
170 return true;
171 }
172 for (int i = 0; i < input.length(); i++) {
173 if (!Character.isWhitespace(input.charAt(i))) {
174 return false;
175 }
176 }
177 return true;
178 }
179}
diff --git a/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java b/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java
deleted file mode 100644
index 48b255f..0000000
--- a/src/main/java/cuchaz/enigma/utils/search/SearchEntry.java
+++ /dev/null
@@ -1,17 +0,0 @@
1package cuchaz.enigma.utils.search;
2
3import java.util.List;
4
5public interface SearchEntry {
6
7 List<String> getSearchableNames();
8
9 /**
10 * Returns a type that uniquely identifies this search entry across possible changes.
11 * This is used for tracking the amount of times this entry has been selected.
12 *
13 * @return a unique identifier for this search entry
14 */
15 String getIdentifier();
16
17}
diff --git a/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java b/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java
deleted file mode 100644
index a51afbb..0000000
--- a/src/main/java/cuchaz/enigma/utils/search/SearchUtil.java
+++ /dev/null
@@ -1,268 +0,0 @@
1package cuchaz.enigma.utils.search;
2
3import java.util.*;
4import java.util.concurrent.Executor;
5import java.util.concurrent.Executors;
6import java.util.concurrent.atomic.AtomicBoolean;
7import java.util.concurrent.atomic.AtomicInteger;
8import java.util.concurrent.locks.Lock;
9import java.util.concurrent.locks.ReentrantLock;
10import java.util.function.BiFunction;
11import java.util.stream.Collectors;
12import java.util.stream.Stream;
13
14import cuchaz.enigma.utils.Pair;
15
16public class SearchUtil<T extends SearchEntry> {
17
18 private final Map<T, Entry<T>> entries = new HashMap<>();
19 private final Map<String, Integer> hitCount = new HashMap<>();
20 private final Executor searchExecutor = Executors.newWorkStealingPool();
21
22 public void add(T entry) {
23 Entry<T> e = Entry.from(entry);
24 entries.put(entry, e);
25 }
26
27 public void add(Entry<T> entry) {
28 entries.put(entry.searchEntry, entry);
29 }
30
31 public void addAll(Collection<T> entries) {
32 this.entries.putAll(entries.parallelStream().collect(Collectors.toMap(e -> e, Entry::from)));
33 }
34
35 public void remove(T entry) {
36 entries.remove(entry);
37 }
38
39 public void clear() {
40 entries.clear();
41 }
42
43 public void clearHits() {
44 hitCount.clear();
45 }
46
47 public Stream<T> search(String term) {
48 return entries.values().parallelStream()
49 .map(e -> new Pair<>(e, e.getScore(term, hitCount.getOrDefault(e.searchEntry.getIdentifier(), 0))))
50 .filter(e -> e.b > 0)
51 .sorted(Comparator.comparingDouble(o -> -o.b))
52 .map(e -> e.a.searchEntry)
53 .sequential();
54 }
55
56 public SearchControl asyncSearch(String term, SearchResultConsumer<T> consumer) {
57 Map<String, Integer> hitCount = new HashMap<>(this.hitCount);
58 Map<T, Entry<T>> entries = new HashMap<>(this.entries);
59 float[] scores = new float[entries.size()];
60 Lock scoresLock = new ReentrantLock();
61 AtomicInteger size = new AtomicInteger();
62 AtomicBoolean control = new AtomicBoolean(false);
63 AtomicInteger elapsed = new AtomicInteger();
64 for (Entry<T> value : entries.values()) {
65 searchExecutor.execute(() -> {
66 try {
67 if (control.get()) return;
68 float score = value.getScore(term, hitCount.getOrDefault(value.searchEntry.getIdentifier(), 0));
69 if (score <= 0) return;
70 score = -score; // sort descending
71 try {
72 scoresLock.lock();
73 if (control.get()) return;
74 int dataSize = size.getAndIncrement();
75 int index = Arrays.binarySearch(scores, 0, dataSize, score);
76 if (index < 0) {
77 index = ~index;
78 }
79 System.arraycopy(scores, index, scores, index + 1, dataSize - index);
80 scores[index] = score;
81 consumer.add(index, value.searchEntry);
82 } finally {
83 scoresLock.unlock();
84 }
85 } finally {
86 elapsed.incrementAndGet();
87 }
88 });
89 }
90
91 return new SearchControl() {
92 @Override
93 public void stop() {
94 control.set(true);
95 }
96
97 @Override
98 public boolean isFinished() {
99 return entries.size() == elapsed.get();
100 }
101
102 @Override
103 public float getProgress() {
104 return (float) elapsed.get() / entries.size();
105 }
106 };
107 }
108
109 public void hit(T entry) {
110 if (entries.containsKey(entry)) {
111 hitCount.compute(entry.getIdentifier(), (_id, i) -> i == null ? 1 : i + 1);
112 }
113 }
114
115 public static final class Entry<T extends SearchEntry> {
116
117 public final T searchEntry;
118 private final String[][] components;
119
120 private Entry(T searchEntry, String[][] components) {
121 this.searchEntry = searchEntry;
122 this.components = components;
123 }
124
125 public float getScore(String term, int hits) {
126 String ucTerm = term.toUpperCase(Locale.ROOT);
127 float maxScore = (float) Arrays.stream(components)
128 .mapToDouble(name -> getScoreFor(ucTerm, name))
129 .max().orElse(0.0);
130 return maxScore * (hits + 1);
131 }
132
133 /**
134 * Computes the score for the given <code>name</code> against the given search term.
135 *
136 * @param term the search term (expected to be upper-case)
137 * @param name the entry name, split at word boundaries (see {@link Entry#wordwiseSplit(String)})
138 * @return the computed score for the entry
139 */
140 private static float getScoreFor(String term, String[] name) {
141 int totalLength = Arrays.stream(name).mapToInt(String::length).sum();
142 float scorePerChar = 1f / totalLength;
143
144 // This map contains a snapshot of all the states the search has
145 // been in. The keys are the remaining characters of the search
146 // term, the values are the maximum scores for that remaining
147 // search term part.
148 Map<String, Float> snapshots = new HashMap<>();
149 snapshots.put(term, 0f);
150
151 // For each component, start at each existing snapshot, searching
152 // for the next longest match, and calculate the new score for each
153 // match length until the maximum. Then the new scores are put back
154 // into the snapshot map.
155 for (int componentIndex = 0; componentIndex < name.length; componentIndex++) {
156 String component = name[componentIndex];
157 float posMultiplier = (name.length - componentIndex) * 0.3f;
158 Map<String, Float> newSnapshots = new HashMap<>();
159 for (Map.Entry<String, Float> snapshot : snapshots.entrySet()) {
160 String remaining = snapshot.getKey();
161 float score = snapshot.getValue();
162 component = component.toUpperCase(Locale.ROOT);
163 int l = compareEqualLength(remaining, component);
164 for (int i = 1; i <= l; i++) {
165 float baseScore = scorePerChar * i;
166 float chainBonus = (i - 1) * 0.5f;
167 merge(newSnapshots, Collections.singletonMap(remaining.substring(i), score + baseScore * posMultiplier + chainBonus), Math::max);
168 }
169 }
170 merge(snapshots, newSnapshots, Math::max);
171 }
172
173 // Only return the score for when the search term was completely
174 // consumed.
175 return snapshots.getOrDefault("", 0f);
176 }
177
178 private static <K, V> void merge(Map<K, V> self, Map<K, V> source, BiFunction<V, V, V> combiner) {
179 source.forEach((k, v) -> self.compute(k, (_k, v1) -> v1 == null ? v : v == null ? v1 : combiner.apply(v, v1)));
180 }
181
182 public static <T extends SearchEntry> Entry<T> from(T e) {
183 String[][] components = e.getSearchableNames().parallelStream()
184 .map(Entry::wordwiseSplit)
185 .toArray(String[][]::new);
186 return new Entry<>(e, components);
187 }
188
189 private static int compareEqualLength(String s1, String s2) {
190 int len = 0;
191 while (len < s1.length() && len < s2.length() && s1.charAt(len) == s2.charAt(len)) {
192 len += 1;
193 }
194 return len;
195 }
196
197 /**
198 * Splits the given input into components, trying to detect word parts.
199 * <p>
200 * Example of how words get split (using <code>|</code> as seperator):
201 * <p><code>MinecraftClientGame -> Minecraft|Client|Game</code></p>
202 * <p><code>HTTPInputStream -> HTTP|Input|Stream</code></p>
203 * <p><code>class_932 -> class|_|932</code></p>
204 * <p><code>X11FontManager -> X|11|Font|Manager</code></p>
205 * <p><code>openHTTPConnection -> open|HTTP|Connection</code></p>
206 * <p><code>open_http_connection -> open|_|http|_|connection</code></p>
207 *
208 * @param input the input to split
209 * @return the resulting components
210 */
211 private static String[] wordwiseSplit(String input) {
212 List<String> list = new ArrayList<>();
213 while (!input.isEmpty()) {
214 int take;
215 if (Character.isLetter(input.charAt(0))) {
216 if (input.length() == 1) {
217 take = 1;
218 } else {
219 boolean nextSegmentIsUppercase = Character.isUpperCase(input.charAt(0)) && Character.isUpperCase(input.charAt(1));
220 if (nextSegmentIsUppercase) {
221 int nextLowercase = 1;
222 while (Character.isUpperCase(input.charAt(nextLowercase))) {
223 nextLowercase += 1;
224 if (nextLowercase == input.length()) {
225 nextLowercase += 1;
226 break;
227 }
228 }
229 take = nextLowercase - 1;
230 } else {
231 int nextUppercase = 1;
232 while (nextUppercase < input.length() && Character.isLowerCase(input.charAt(nextUppercase))) {
233 nextUppercase += 1;
234 }
235 take = nextUppercase;
236 }
237 }
238 } else if (Character.isDigit(input.charAt(0))) {
239 int nextNonNum = 1;
240 while (nextNonNum < input.length() && Character.isLetter(input.charAt(nextNonNum)) && !Character.isLowerCase(input.charAt(nextNonNum))) {
241 nextNonNum += 1;
242 }
243 take = nextNonNum;
244 } else {
245 take = 1;
246 }
247 list.add(input.substring(0, take));
248 input = input.substring(take);
249 }
250 return list.toArray(new String[0]);
251 }
252
253 }
254
255 @FunctionalInterface
256 public interface SearchResultConsumer<T extends SearchEntry> {
257 void add(int index, T entry);
258 }
259
260 public interface SearchControl {
261 void stop();
262
263 boolean isFinished();
264
265 float getProgress();
266 }
267
268}