summaryrefslogtreecommitdiff
path: root/enigma/src/main/java/cuchaz
diff options
context:
space:
mode:
authorGravatar 2xsaiko2020-06-03 20:16:10 +0200
committerGravatar GitHub2020-06-03 19:16:10 +0100
commit5a286d58e740f1aa5944488c602f5abc1318f6ca (patch)
treedfde9eff0c744906b3571390af0f6a6e3be92a91 /enigma/src/main/java/cuchaz
parentRefactor MenuBar (#251) (diff)
downloadenigma-fork-5a286d58e740f1aa5944488c602f5abc1318f6ca.tar.gz
enigma-fork-5a286d58e740f1aa5944488c602f5abc1318f6ca.tar.xz
enigma-fork-5a286d58e740f1aa5944488c602f5abc1318f6ca.zip
Editor tabs (#238)
* Split into modules * Add validation utils from patch-1 branch * Tabs, iteration 1 * Delete RefreshMode * Load initial code asynchronously * Formatting * Don't do anything when close() gets called multiple times * Add scroll pane to editor * Fix getActiveEditor() * Rename components to more descriptive editorScrollPanes * Move ClassHandle and related types out of gui package * Fix tab title bar and other files not updating when changing mappings * Fix compilation errors * Start adding renaming functionality to new panel * Scale validation error marker * Make most user input validation use ValidationContext * Fix line numbers not displaying * Move CodeReader.navigateToToken into PanelEditor * Add close button on tabs * Remove TODO, it's fast enough * Remove JS script action for 2 seconds faster startup * Add comment on why the action is removed * ClassHandle/ClassHandleProvider documentation * Fix language file formatting * Bulk tab closing operations * Fix crash when renaming class and not connected to server * Fix caret jumping to the end of the file when opening * Increase identifier panel size * Make popup menu text translatable * Fix formatting * Fix compilation issues * CovertTextField -> ConvertingTextField * Retain formatting using spaces * Add de_de.json * Better decompilation error handling * Fix some caret related NPEs * Localization * Close editor on classhandle delete & fix onInvalidate not running on the Swing thread * Fix crash when trying to close a tab from onDeleted class handle listener Co-authored-by: Runemoro <runemoro1@gmail.com>
Diffstat (limited to 'enigma/src/main/java/cuchaz')
-rw-r--r--enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java12
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java108
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java35
-rw-r--r--enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java445
-rw-r--r--enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java36
-rw-r--r--enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java157
-rw-r--r--enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java7
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java8
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java28
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java79
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java39
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java24
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java50
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java19
-rw-r--r--enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java15
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/I18n.java55
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/Result.java108
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/Utils.java21
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java48
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java45
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java37
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java34
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java9
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java78
24 files changed, 1345 insertions, 152 deletions
diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java
index 320f945..9d6bc4e 100644
--- a/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java
+++ b/enigma/src/main/java/cuchaz/enigma/analysis/EntryReference.java
@@ -11,19 +11,19 @@
11 11
12package cuchaz.enigma.analysis; 12package cuchaz.enigma.analysis;
13 13
14import java.util.Arrays;
15import java.util.List;
16import java.util.Objects;
17
14import cuchaz.enigma.translation.Translatable; 18import cuchaz.enigma.translation.Translatable;
15import cuchaz.enigma.translation.Translator; 19import cuchaz.enigma.translation.Translator;
20import cuchaz.enigma.translation.mapping.EntryMap;
16import cuchaz.enigma.translation.mapping.EntryMapping; 21import cuchaz.enigma.translation.mapping.EntryMapping;
17import cuchaz.enigma.translation.mapping.EntryResolver; 22import cuchaz.enigma.translation.mapping.EntryResolver;
18import cuchaz.enigma.translation.mapping.EntryMap;
19import cuchaz.enigma.translation.representation.entry.ClassEntry; 23import cuchaz.enigma.translation.representation.entry.ClassEntry;
20import cuchaz.enigma.translation.representation.entry.Entry; 24import cuchaz.enigma.translation.representation.entry.Entry;
21import cuchaz.enigma.translation.representation.entry.MethodEntry; 25import cuchaz.enigma.translation.representation.entry.MethodEntry;
22 26
23import java.util.Arrays;
24import java.util.List;
25import java.util.Objects;
26
27public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements Translatable { 27public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements Translatable {
28 28
29 private static final List<String> CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static"); 29 private static final List<String> CONSTRUCTOR_NON_NAMES = Arrays.asList("this", "super", "static");
@@ -100,6 +100,8 @@ public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements T
100 } 100 }
101 101
102 public boolean equals(EntryReference<?, ?> other) { 102 public boolean equals(EntryReference<?, ?> other) {
103 if (other == null) return false;
104
103 // check entry first 105 // check entry first
104 boolean isEntrySame = entry.equals(other.entry); 106 boolean isEntrySame = entry.equals(other.entry);
105 if (!isEntrySame) { 107 if (!isEntrySame) {
diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java
new file mode 100644
index 0000000..326197d
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandle.java
@@ -0,0 +1,108 @@
1package cuchaz.enigma.classhandle;
2
3import java.util.concurrent.CompletableFuture;
4
5import javax.annotation.Nullable;
6
7import cuchaz.enigma.events.ClassHandleListener;
8import cuchaz.enigma.source.DecompiledClassSource;
9import cuchaz.enigma.source.Source;
10import cuchaz.enigma.translation.representation.entry.ClassEntry;
11import cuchaz.enigma.utils.Result;
12
13/**
14 * A handle to a class file. Can be treated similarly to a handle to a file.
15 * This type allows for accessing decompiled classes and being notified when
16 * mappings for the class it belongs to changes.
17 *
18 * @see ClassHandleProvider
19 */
20public interface ClassHandle extends AutoCloseable {
21
22 /**
23 * Gets the reference to this class. This is always obfuscated, for example
24 * {@code net/minecraft/class_1000}.
25 *
26 * @return the obfuscated class reference
27 * @throws IllegalStateException if the class handle is closed
28 */
29 ClassEntry getRef();
30
31 /**
32 * Gets the deobfuscated reference to this class, if any.
33 *
34 * @return the deobfuscated reference, or {@code null} if the class is not
35 * mapped
36 * @throws IllegalStateException if the class handle is closed
37 */
38 @Nullable
39 ClassEntry getDeobfRef();
40
41 /**
42 * Gets the class source, or the error generated while decompiling the
43 * class, asynchronously. If this class has already finished decompiling,
44 * this will return an already completed future.
45 *
46 * @return the class source
47 * @throws IllegalStateException if the class handle is closed
48 */
49 CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSource();
50
51 /**
52 * Gets the class source without any decoration, or the error generated
53 * while decompiling the class, asynchronously. This is the raw source from
54 * the decompiler and will not be deobfuscated, and does not contain any
55 * Javadoc comments added via mappings. If this class has already finished
56 * decompiling, this will return an already completed future.
57 *
58 * @return the uncommented class source
59 * @throws IllegalStateException if the class handle is closed
60 * @see ClassHandle#getSource()
61 */
62 CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSource();
63
64 void invalidate();
65
66 void invalidateMapped();
67
68 void invalidateJavadoc();
69
70 /**
71 * Adds a listener for this class handle.
72 *
73 * @param listener the listener to add
74 * @see ClassHandleListener
75 */
76 void addListener(ClassHandleListener listener);
77
78 /**
79 * Removes a previously added listener (with
80 * {@link ClassHandle#addListener(ClassHandleListener)}) from this class
81 * handle.
82 *
83 * @param listener the listener to remove
84 */
85 void removeListener(ClassHandleListener listener);
86
87 /**
88 * Copies this class handle. The new class handle points to the same class,
89 * but is independent from this class handle in every other aspect.
90 * Specifically, any listeners will not be copied to the new class handle.
91 *
92 * @return a copy of this class handle
93 * @throws IllegalStateException if the class handle is closed
94 */
95 ClassHandle copy();
96
97 /**
98 * {@inheritDoc}
99 *
100 * <p>Specifically, for class handles, this means that most methods on the
101 * handle will throw an exception if called, that the handle will no longer
102 * receive any events over any added listeners, and the handle will no
103 * longer be able to be copied.
104 */
105 @Override
106 void close();
107
108}
diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java
new file mode 100644
index 0000000..a11b9dc
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleError.java
@@ -0,0 +1,35 @@
1package cuchaz.enigma.classhandle;
2
3import java.io.ByteArrayOutputStream;
4import java.io.PrintStream;
5
6import javax.annotation.Nullable;
7
8public final class ClassHandleError {
9
10 public final Type type;
11 public final Throwable cause;
12
13 private ClassHandleError(Type type, Throwable cause) {
14 this.type = type;
15 this.cause = cause;
16 }
17
18 @Nullable
19 public String getStackTrace() {
20 if (cause == null) return null;
21 ByteArrayOutputStream os = new ByteArrayOutputStream();
22 PrintStream ps = new PrintStream(os);
23 cause.printStackTrace(ps);
24 return os.toString();
25 }
26
27 public static ClassHandleError decompile(Throwable cause) {
28 return new ClassHandleError(Type.DECOMPILE, cause);
29 }
30
31 public enum Type {
32 DECOMPILE,
33 }
34
35} \ No newline at end of file
diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java
new file mode 100644
index 0000000..2d9b52d
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java
@@ -0,0 +1,445 @@
1package cuchaz.enigma.classhandle;
2
3import java.util.*;
4import java.util.concurrent.CompletableFuture;
5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.Executors;
7import java.util.concurrent.TimeUnit;
8import java.util.concurrent.atomic.AtomicInteger;
9import java.util.concurrent.locks.ReadWriteLock;
10import java.util.concurrent.locks.ReentrantReadWriteLock;
11
12import javax.annotation.Nullable;
13
14import cuchaz.enigma.Enigma;
15import cuchaz.enigma.EnigmaProject;
16import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
17import cuchaz.enigma.events.ClassHandleListener;
18import cuchaz.enigma.events.ClassHandleListener.InvalidationType;
19import cuchaz.enigma.source.*;
20import cuchaz.enigma.translation.representation.entry.ClassEntry;
21import cuchaz.enigma.utils.Result;
22import org.objectweb.asm.tree.ClassNode;
23
24import static cuchaz.enigma.utils.Utils.withLock;
25
26public final class ClassHandleProvider {
27
28 private final EnigmaProject project;
29
30 private final ExecutorService pool = Executors.newWorkStealingPool();
31 private DecompilerService ds;
32 private Decompiler decompiler;
33
34 private final Map<ClassEntry, Entry> handles = new HashMap<>();
35
36 private final ReadWriteLock lock = new ReentrantReadWriteLock();
37
38 public ClassHandleProvider(EnigmaProject project, DecompilerService ds) {
39 this.project = project;
40 this.ds = ds;
41 this.decompiler = createDecompiler();
42 }
43
44 /**
45 * Open a class by entry. Schedules decompilation immediately if this is the
46 * only handle to the class.
47 *
48 * @param entry the entry of the class to open
49 * @return a handle to the class, {@code null} if a class by that name does
50 * not exist
51 */
52 @Nullable
53 public ClassHandle openClass(ClassEntry entry) {
54 if (!project.getJarIndex().getEntryIndex().hasClass(entry)) return null;
55
56 return withLock(lock.writeLock(), () -> {
57 Entry e = handles.computeIfAbsent(entry, entry1 -> new Entry(this, entry1));
58 return e.createHandle();
59 });
60 }
61
62 /**
63 * Set the decompiler service to use when decompiling classes. Invalidates
64 * all currently open classes.
65 *
66 * <p>If the current decompiler service equals the old one, no classes will
67 * be invalidated.
68 *
69 * @param ds the decompiler service to use
70 */
71 public void setDecompilerService(DecompilerService ds) {
72 if (this.ds.equals(ds)) return;
73
74 this.ds = ds;
75 this.decompiler = createDecompiler();
76 withLock(lock.readLock(), () -> {
77 handles.values().forEach(Entry::invalidate);
78 });
79 }
80
81 /**
82 * Gets the current decompiler service in use.
83 *
84 * @return the current decompiler service
85 */
86 public DecompilerService getDecompilerService() {
87 return ds;
88 }
89
90 private Decompiler createDecompiler() {
91 return ds.create(name -> {
92 ClassNode node = project.getClassCache().getClassNode(name);
93
94 if (node == null) {
95 return null;
96 }
97
98 ClassNode fixedNode = new ClassNode();
99 node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, project.getJarIndex()));
100 return fixedNode;
101 }, new SourceSettings(true, true));
102 }
103
104 /**
105 * Invalidates all mappings. This causes all open class handles to be
106 * re-remapped.
107 */
108 public void invalidateMapped() {
109 withLock(lock.readLock(), () -> {
110 handles.values().forEach(Entry::invalidateMapped);
111 });
112 }
113
114 /**
115 * Invalidates mappings for a single class. Note that this does not
116 * invalidate any mappings of other classes where this class is used, so
117 * this should not be used to notify that the mapped name for this class has
118 * changed.
119 *
120 * @param entry the class entry to invalidate
121 */
122 public void invalidateMapped(ClassEntry entry) {
123 withLock(lock.readLock(), () -> {
124 Entry e = handles.get(entry);
125 if (e != null) {
126 e.invalidateMapped();
127 }
128 });
129 }
130
131 /**
132 * Invalidates javadoc for a single class. This also causes the class to be
133 * remapped again.
134 *
135 * @param entry the class entry to invalidate
136 */
137 public void invalidateJavadoc(ClassEntry entry) {
138 withLock(lock.readLock(), () -> {
139 Entry e = handles.get(entry);
140 if (e != null) {
141 e.invalidateJavadoc();
142 }
143 });
144 }
145
146 private void deleteEntry(Entry entry) {
147 withLock(lock.writeLock(), () -> {
148 handles.remove(entry.entry);
149 });
150 }
151
152 /**
153 * Destroy this class handle provider. The decompiler threads will try to
154 * shutdown cleanly, and then every open class handle will also be deleted.
155 * This causes {@link ClassHandleListener#onDeleted(ClassHandle)} to get
156 * called.
157 *
158 * <p>After this method is called, this class handle provider can no longer
159 * be used.
160 */
161 public void destroy() {
162 pool.shutdown();
163 try {
164 pool.awaitTermination(30, TimeUnit.SECONDS);
165 } catch (InterruptedException e) {
166 throw new RuntimeException(e);
167 }
168
169 withLock(lock.writeLock(), () -> {
170 handles.values().forEach(Entry::destroy);
171 handles.clear();
172 });
173 }
174
175 private static final class Entry {
176
177 private final ClassHandleProvider p;
178 private final ClassEntry entry;
179 private ClassEntry deobfRef;
180 private final List<ClassHandleImpl> handles = new ArrayList<>();
181 private Result<Source, ClassHandleError> uncommentedSource;
182 private Result<DecompiledClassSource, ClassHandleError> source;
183
184 private final List<CompletableFuture<Result<Source, ClassHandleError>>> waitingUncommentedSources = Collections.synchronizedList(new ArrayList<>());
185 private final List<CompletableFuture<Result<DecompiledClassSource, ClassHandleError>>> waitingSources = Collections.synchronizedList(new ArrayList<>());
186
187 private final AtomicInteger decompileVersion = new AtomicInteger();
188 private final AtomicInteger javadocVersion = new AtomicInteger();
189 private final AtomicInteger indexVersion = new AtomicInteger();
190 private final AtomicInteger mappedVersion = new AtomicInteger();
191
192 private final ReadWriteLock lock = new ReentrantReadWriteLock();
193
194 private Entry(ClassHandleProvider p, ClassEntry entry) {
195 this.p = p;
196 this.entry = entry;
197 this.deobfRef = p.project.getMapper().deobfuscate(entry);
198 invalidate();
199 }
200
201 public ClassHandleImpl createHandle() {
202 ClassHandleImpl handle = new ClassHandleImpl(this);
203 withLock(lock.writeLock(), () -> {
204 handles.add(handle);
205 });
206 return handle;
207 }
208
209 @Nullable
210 public ClassEntry getDeobfRef() {
211 return deobfRef;
212 }
213
214 private void checkDeobfRefForUpdate() {
215 ClassEntry newDeobf = p.project.getMapper().deobfuscate(entry);
216 if (!Objects.equals(deobfRef, newDeobf)) {
217 deobfRef = newDeobf;
218 // copy the list so we don't call event listener code with the lock active
219 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDeobfRefChanged(newDeobf));
220 }
221 }
222
223 public void invalidate() {
224 checkDeobfRefForUpdate();
225 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.FULL));
226 continueMapSource(continueIndexSource(continueInsertJavadoc(decompile())));
227 }
228
229 public void invalidateJavadoc() {
230 checkDeobfRefForUpdate();
231 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.JAVADOC));
232 continueMapSource(continueIndexSource(continueInsertJavadoc(CompletableFuture.completedFuture(uncommentedSource))));
233 }
234
235 public void invalidateMapped() {
236 checkDeobfRefForUpdate();
237 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onInvalidate(InvalidationType.MAPPINGS));
238 continueMapSource(CompletableFuture.completedFuture(source));
239 }
240
241 private CompletableFuture<Result<Source, ClassHandleError>> decompile() {
242 int v = decompileVersion.incrementAndGet();
243 return CompletableFuture.supplyAsync(() -> {
244 if (decompileVersion.get() != v) return null;
245
246 Result<Source, ClassHandleError> _uncommentedSource;
247 try {
248 _uncommentedSource = Result.ok(p.decompiler.getSource(entry.getFullName()));
249 } catch (Throwable e) {
250 return Result.err(ClassHandleError.decompile(e));
251 }
252 Result<Source, ClassHandleError> uncommentedSource = _uncommentedSource;
253 Entry.this.uncommentedSource = uncommentedSource;
254 Entry.this.waitingUncommentedSources.forEach(f -> f.complete(uncommentedSource));
255 Entry.this.waitingUncommentedSources.clear();
256 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onUncommentedSourceChanged(uncommentedSource));
257 return uncommentedSource;
258 }, p.pool);
259 }
260
261 private CompletableFuture<Result<Source, ClassHandleError>> continueInsertJavadoc(CompletableFuture<Result<Source, ClassHandleError>> f) {
262 int v = javadocVersion.incrementAndGet();
263 return f.thenApplyAsync(res -> {
264 if (res == null || javadocVersion.get() != v) return null;
265 Result<Source, ClassHandleError> jdSource = res.map(s -> s.addJavadocs(p.project.getMapper()));
266 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onDocsChanged(jdSource));
267 return jdSource;
268 }, p.pool);
269 }
270
271 private CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> continueIndexSource(CompletableFuture<Result<Source, ClassHandleError>> f) {
272 int v = indexVersion.incrementAndGet();
273 return f.thenApplyAsync(res -> {
274 if (res == null || indexVersion.get() != v) return null;
275 return res.andThen(jdSource -> {
276 SourceIndex index = jdSource.index();
277 index.resolveReferences(p.project.getMapper().getObfResolver());
278 DecompiledClassSource source = new DecompiledClassSource(entry, index);
279 return Result.ok(source);
280 });
281 }, p.pool);
282 }
283
284 private void continueMapSource(CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> f) {
285 int v = mappedVersion.incrementAndGet();
286 f.thenAcceptAsync(res -> {
287 if (res == null || mappedVersion.get() != v) return;
288 res = res.map(source -> {
289 source.remapSource(p.project, p.project.getMapper().getDeobfuscator());
290 return source;
291 });
292 Entry.this.source = res;
293 Entry.this.waitingSources.forEach(s -> s.complete(source));
294 Entry.this.waitingSources.clear();
295 withLock(lock.readLock(), () -> new ArrayList<>(handles)).forEach(h -> h.onMappedSourceChanged(source));
296 }, p.pool);
297 }
298
299 public void closeHandle(ClassHandleImpl classHandle) {
300 classHandle.destroy();
301 withLock(lock.writeLock(), () -> {
302 handles.remove(classHandle);
303 if (handles.isEmpty()) {
304 p.deleteEntry(this);
305 }
306 });
307 }
308
309 public void destroy() {
310 withLock(lock.writeLock(), () -> {
311 handles.forEach(ClassHandleImpl::destroy);
312 handles.clear();
313 });
314 }
315
316 public CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSourceAsync() {
317 if (uncommentedSource != null) {
318 return CompletableFuture.completedFuture(uncommentedSource);
319 } else {
320 CompletableFuture<Result<Source, ClassHandleError>> f = new CompletableFuture<>();
321 waitingUncommentedSources.add(f);
322 return f;
323 }
324 }
325
326 public CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSourceAsync() {
327 if (source != null) {
328 return CompletableFuture.completedFuture(source);
329 } else {
330 CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> f = new CompletableFuture<>();
331 waitingSources.add(f);
332 return f;
333 }
334 }
335 }
336
337 private static final class ClassHandleImpl implements ClassHandle {
338
339 private final Entry entry;
340
341 private boolean valid = true;
342
343 private final Set<ClassHandleListener> listeners = new HashSet<>();
344
345 private ClassHandleImpl(Entry entry) {
346 this.entry = entry;
347 }
348
349 @Override
350 public ClassEntry getRef() {
351 checkValid();
352 return entry.entry;
353 }
354
355 @Nullable
356 @Override
357 public ClassEntry getDeobfRef() {
358 checkValid();
359 // cache this?
360 return entry.getDeobfRef();
361 }
362
363 @Override
364 public CompletableFuture<Result<DecompiledClassSource, ClassHandleError>> getSource() {
365 checkValid();
366 return entry.getSourceAsync();
367 }
368
369 @Override
370 public CompletableFuture<Result<Source, ClassHandleError>> getUncommentedSource() {
371 checkValid();
372 return entry.getUncommentedSourceAsync();
373 }
374
375 @Override
376 public void invalidate() {
377 checkValid();
378 this.entry.invalidate();
379 }
380
381 @Override
382 public void invalidateMapped() {
383 checkValid();
384 this.entry.invalidateMapped();
385 }
386
387 @Override
388 public void invalidateJavadoc() {
389 checkValid();
390 this.entry.invalidateJavadoc();
391 }
392
393 public void onUncommentedSourceChanged(Result<Source, ClassHandleError> source) {
394 listeners.forEach(l -> l.onUncommentedSourceChanged(this, source));
395 }
396
397 public void onDocsChanged(Result<Source, ClassHandleError> source) {
398 listeners.forEach(l -> l.onDocsChanged(this, source));
399 }
400
401 public void onMappedSourceChanged(Result<DecompiledClassSource, ClassHandleError> source) {
402 listeners.forEach(l -> l.onMappedSourceChanged(this, source));
403 }
404
405 public void onInvalidate(InvalidationType t) {
406 listeners.forEach(l -> l.onInvalidate(this, t));
407 }
408
409 public void onDeobfRefChanged(ClassEntry newDeobf) {
410 listeners.forEach(l -> l.onDeobfRefChanged(this, newDeobf));
411 }
412
413 @Override
414 public void addListener(ClassHandleListener listener) {
415 listeners.add(listener);
416 }
417
418 @Override
419 public void removeListener(ClassHandleListener listener) {
420 listeners.remove(listener);
421 }
422
423 @Override
424 public ClassHandle copy() {
425 checkValid();
426 return entry.createHandle();
427 }
428
429 @Override
430 public void close() {
431 if (valid) entry.closeHandle(this);
432 }
433
434 private void checkValid() {
435 if (!valid) throw new IllegalStateException("Class handle no longer valid");
436 }
437
438 public void destroy() {
439 listeners.forEach(l -> l.onDeleted(this));
440 valid = false;
441 }
442
443 }
444
445}
diff --git a/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java b/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java
new file mode 100644
index 0000000..61fea4e
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/events/ClassHandleListener.java
@@ -0,0 +1,36 @@
1package cuchaz.enigma.events;
2
3import cuchaz.enigma.classhandle.ClassHandle;
4import cuchaz.enigma.classhandle.ClassHandleError;
5import cuchaz.enigma.source.DecompiledClassSource;
6import cuchaz.enigma.source.Source;
7import cuchaz.enigma.translation.representation.entry.ClassEntry;
8import cuchaz.enigma.utils.Result;
9
10public interface ClassHandleListener {
11
12 default void onDeobfRefChanged(ClassHandle h, ClassEntry deobfRef) {
13 }
14
15 default void onUncommentedSourceChanged(ClassHandle h, Result<Source, ClassHandleError> res) {
16 }
17
18 default void onDocsChanged(ClassHandle h, Result<Source, ClassHandleError> res) {
19 }
20
21 default void onMappedSourceChanged(ClassHandle h, Result<DecompiledClassSource, ClassHandleError> res) {
22 }
23
24 default void onInvalidate(ClassHandle h, InvalidationType t) {
25 }
26
27 default void onDeleted(ClassHandle h) {
28 }
29
30 enum InvalidationType {
31 FULL,
32 JAVADOC,
33 MAPPINGS,
34 }
35
36}
diff --git a/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java
new file mode 100644
index 0000000..85fba50
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/source/DecompiledClassSource.java
@@ -0,0 +1,157 @@
1package cuchaz.enigma.source;
2
3import java.util.*;
4
5import javax.annotation.Nullable;
6
7import cuchaz.enigma.EnigmaProject;
8import cuchaz.enigma.EnigmaServices;
9import cuchaz.enigma.analysis.EntryReference;
10import cuchaz.enigma.api.service.NameProposalService;
11import cuchaz.enigma.translation.LocalNameGenerator;
12import cuchaz.enigma.translation.Translator;
13import cuchaz.enigma.translation.mapping.EntryRemapper;
14import cuchaz.enigma.translation.mapping.ResolutionStrategy;
15import cuchaz.enigma.translation.representation.TypeDescriptor;
16import cuchaz.enigma.translation.representation.entry.ClassEntry;
17import cuchaz.enigma.translation.representation.entry.Entry;
18import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry;
19
20public class DecompiledClassSource {
21 private final ClassEntry classEntry;
22
23 private final SourceIndex obfuscatedIndex;
24 private SourceIndex remappedIndex;
25
26 private final Map<RenamableTokenType, Collection<Token>> highlightedTokens = new EnumMap<>(RenamableTokenType.class);
27
28 public DecompiledClassSource(ClassEntry classEntry, SourceIndex index) {
29 this.classEntry = classEntry;
30 this.obfuscatedIndex = index;
31 this.remappedIndex = index;
32 }
33
34 public static DecompiledClassSource text(ClassEntry classEntry, String text) {
35 return new DecompiledClassSource(classEntry, new SourceIndex(text));
36 }
37
38 public void remapSource(EnigmaProject project, Translator translator) {
39 highlightedTokens.clear();
40
41 SourceRemapper remapper = new SourceRemapper(obfuscatedIndex.getSource(), obfuscatedIndex.referenceTokens());
42
43 SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> remapToken(project, token, movedToken, translator));
44 remappedIndex = obfuscatedIndex.remapTo(remapResult);
45 }
46
47 private String remapToken(EnigmaProject project, Token token, Token movedToken, Translator translator) {
48 EntryReference<Entry<?>, Entry<?>> reference = obfuscatedIndex.getReference(token);
49
50 Entry<?> entry = reference.getNameableEntry();
51 Entry<?> translatedEntry = translator.translate(entry);
52
53 if (project.isRenamable(reference)) {
54 if (isDeobfuscated(entry, translatedEntry)) {
55 highlightToken(movedToken, RenamableTokenType.DEOBFUSCATED);
56 return translatedEntry.getSourceRemapName();
57 } else {
58 Optional<String> proposedName = proposeName(project, entry);
59 if (proposedName.isPresent()) {
60 highlightToken(movedToken, RenamableTokenType.PROPOSED);
61 return proposedName.get();
62 }
63
64 highlightToken(movedToken, RenamableTokenType.OBFUSCATED);
65 }
66 }
67
68 String defaultName = generateDefaultName(translatedEntry);
69 if (defaultName != null) {
70 return defaultName;
71 }
72
73 return null;
74 }
75
76 private Optional<String> proposeName(EnigmaProject project, Entry<?> entry) {
77 EnigmaServices services = project.getEnigma().getServices();
78
79 return services.get(NameProposalService.TYPE).stream().flatMap(nameProposalService -> {
80 EntryRemapper mapper = project.getMapper();
81 Collection<Entry<?>> resolved = mapper.getObfResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT);
82
83 return resolved.stream()
84 .map(e -> nameProposalService.proposeName(e, mapper))
85 .filter(Optional::isPresent)
86 .map(Optional::get);
87 }).findFirst();
88 }
89
90 @Nullable
91 private String generateDefaultName(Entry<?> entry) {
92 if (entry instanceof LocalVariableDefEntry) {
93 LocalVariableDefEntry localVariable = (LocalVariableDefEntry) entry;
94
95 int index = localVariable.getIndex();
96 if (localVariable.isArgument()) {
97 List<TypeDescriptor> arguments = localVariable.getParent().getDesc().getArgumentDescs();
98 return LocalNameGenerator.generateArgumentName(index, localVariable.getDesc(), arguments);
99 } else {
100 return LocalNameGenerator.generateLocalVariableName(index, localVariable.getDesc());
101 }
102 }
103
104 return null;
105 }
106
107 private boolean isDeobfuscated(Entry<?> entry, Entry<?> translatedEntry) {
108 return !entry.getName().equals(translatedEntry.getName());
109 }
110
111 public ClassEntry getEntry() {
112 return classEntry;
113 }
114
115 public SourceIndex getIndex() {
116 return remappedIndex;
117 }
118
119 public Map<RenamableTokenType, Collection<Token>> getHighlightedTokens() {
120 return highlightedTokens;
121 }
122
123 private void highlightToken(Token token, RenamableTokenType highlightType) {
124 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token);
125 }
126
127 public int getObfuscatedOffset(int deobfOffset) {
128 return getOffset(remappedIndex, obfuscatedIndex, deobfOffset);
129 }
130
131 public int getDeobfuscatedOffset(int obfOffset) {
132 return getOffset(obfuscatedIndex, remappedIndex, obfOffset);
133 }
134
135 private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fromOffset) {
136 int relativeOffset = 0;
137
138 Iterator<Token> fromTokenItr = fromIndex.referenceTokens().iterator();
139 Iterator<Token> toTokenItr = toIndex.referenceTokens().iterator();
140 while (fromTokenItr.hasNext() && toTokenItr.hasNext()) {
141 Token fromToken = fromTokenItr.next();
142 Token toToken = toTokenItr.next();
143 if (fromToken.end > fromOffset) {
144 break;
145 }
146
147 relativeOffset = toToken.end - fromToken.end;
148 }
149
150 return fromOffset + relativeOffset;
151 }
152
153 @Override
154 public String toString() {
155 return remappedIndex.getSource();
156 }
157}
diff --git a/enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java b/enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java
new file mode 100644
index 0000000..c63aad9
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/source/RenamableTokenType.java
@@ -0,0 +1,7 @@
1package cuchaz.enigma.source;
2
3public enum RenamableTokenType {
4 OBFUSCATED,
5 DEOBFUSCATED,
6 PROPOSED
7}
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java
index 18c966c..dec75ff 100644
--- a/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java
+++ b/enigma/src/main/java/cuchaz/enigma/translation/LocalNameGenerator.java
@@ -1,18 +1,18 @@
1package cuchaz.enigma.translation; 1package cuchaz.enigma.translation;
2 2
3import cuchaz.enigma.translation.mapping.NameValidator;
4import cuchaz.enigma.translation.representation.TypeDescriptor;
5
6import java.util.Collection; 3import java.util.Collection;
7import java.util.Locale; 4import java.util.Locale;
8 5
6import cuchaz.enigma.translation.mapping.IdentifierValidation;
7import cuchaz.enigma.translation.representation.TypeDescriptor;
8
9public class LocalNameGenerator { 9public class LocalNameGenerator {
10 public static String generateArgumentName(int index, TypeDescriptor desc, Collection<TypeDescriptor> arguments) { 10 public static String generateArgumentName(int index, TypeDescriptor desc, Collection<TypeDescriptor> arguments) {
11 boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1; 11 boolean uniqueType = arguments.stream().filter(desc::equals).count() <= 1;
12 String translatedName; 12 String translatedName;
13 int nameIndex = index + 1; 13 int nameIndex = index + 1;
14 StringBuilder nameBuilder = new StringBuilder(getTypeName(desc)); 14 StringBuilder nameBuilder = new StringBuilder(getTypeName(desc));
15 if (!uniqueType || NameValidator.isReserved(nameBuilder.toString())) { 15 if (!uniqueType || IdentifierValidation.isReservedMethodName(nameBuilder.toString())) {
16 nameBuilder.append(nameIndex); 16 nameBuilder.append(nameIndex);
17 } 17 }
18 translatedName = nameBuilder.toString(); 18 translatedName = nameBuilder.toString();
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java
index 1dd7eac..932b5bb 100644
--- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java
+++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java
@@ -1,5 +1,10 @@
1package cuchaz.enigma.translation.mapping; 1package cuchaz.enigma.translation.mapping;
2 2
3import java.util.Collection;
4import java.util.stream.Stream;
5
6import javax.annotation.Nullable;
7
3import cuchaz.enigma.analysis.index.JarIndex; 8import cuchaz.enigma.analysis.index.JarIndex;
4import cuchaz.enigma.translation.MappingTranslator; 9import cuchaz.enigma.translation.MappingTranslator;
5import cuchaz.enigma.translation.Translatable; 10import cuchaz.enigma.translation.Translatable;
@@ -8,10 +13,7 @@ import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
8import cuchaz.enigma.translation.mapping.tree.EntryTree; 13import cuchaz.enigma.translation.mapping.tree.EntryTree;
9import cuchaz.enigma.translation.mapping.tree.HashEntryTree; 14import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
10import cuchaz.enigma.translation.representation.entry.Entry; 15import cuchaz.enigma.translation.representation.entry.Entry;
11 16import cuchaz.enigma.utils.validation.ValidationContext;
12import javax.annotation.Nullable;
13import java.util.Collection;
14import java.util.stream.Stream;
15 17
16public class EntryRemapper { 18public class EntryRemapper {
17 private final DeltaTrackingTree<EntryMapping> obfToDeobf; 19 private final DeltaTrackingTree<EntryMapping> obfToDeobf;
@@ -39,26 +41,32 @@ public class EntryRemapper {
39 return new EntryRemapper(index, new HashEntryTree<>()); 41 return new EntryRemapper(index, new HashEntryTree<>());
40 } 42 }
41 43
42 public <E extends Entry<?>> void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping) { 44 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping) {
43 mapFromObf(obfuscatedEntry, deobfMapping, true); 45 mapFromObf(vc, obfuscatedEntry, deobfMapping, true);
44 } 46 }
45 47
46 public <E extends Entry<?>> void mapFromObf(E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) { 48 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming) {
49 mapFromObf(vc, obfuscatedEntry, deobfMapping, renaming, false);
50 }
51
52 public <E extends Entry<?>> void mapFromObf(ValidationContext vc, E obfuscatedEntry, @Nullable EntryMapping deobfMapping, boolean renaming, boolean validateOnly) {
47 Collection<E> resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST); 53 Collection<E> resolvedEntries = obfResolver.resolveEntry(obfuscatedEntry, renaming ? ResolutionStrategy.RESOLVE_ROOT : ResolutionStrategy.RESOLVE_CLOSEST);
48 54
49 if (renaming && deobfMapping != null) { 55 if (renaming && deobfMapping != null) {
50 for (E resolvedEntry : resolvedEntries) { 56 for (E resolvedEntry : resolvedEntries) {
51 validator.validateRename(resolvedEntry, deobfMapping.getTargetName()); 57 validator.validateRename(vc, resolvedEntry, deobfMapping.getTargetName());
52 } 58 }
53 } 59 }
54 60
61 if (validateOnly || !vc.canProceed()) return;
62
55 for (E resolvedEntry : resolvedEntries) { 63 for (E resolvedEntry : resolvedEntries) {
56 obfToDeobf.insert(resolvedEntry, deobfMapping); 64 obfToDeobf.insert(resolvedEntry, deobfMapping);
57 } 65 }
58 } 66 }
59 67
60 public void removeByObf(Entry<?> obfuscatedEntry) { 68 public void removeByObf(ValidationContext vc, Entry<?> obfuscatedEntry) {
61 mapFromObf(obfuscatedEntry, null); 69 mapFromObf(vc, obfuscatedEntry, null);
62 } 70 }
63 71
64 @Nullable 72 @Nullable
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java
new file mode 100644
index 0000000..097c9e9
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IdentifierValidation.java
@@ -0,0 +1,79 @@
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.translation.mapping;
13
14import java.util.Arrays;
15import java.util.List;
16
17import cuchaz.enigma.utils.validation.Message;
18import cuchaz.enigma.utils.validation.StandardValidation;
19import cuchaz.enigma.utils.validation.ValidationContext;
20
21public final class IdentifierValidation {
22
23 private IdentifierValidation() {
24 }
25
26 private static final List<String> ILLEGAL_IDENTIFIERS = Arrays.asList(
27 "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized",
28 "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte",
29 "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch",
30 "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally",
31 "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_"
32 );
33
34 public static boolean validateClassName(ValidationContext vc, String name) {
35 if (!StandardValidation.notBlank(vc, name)) return false;
36 String[] parts = name.split("/");
37 for (String part : parts) {
38 validateIdentifier(vc, part);
39 }
40 return true;
41 }
42
43 public static boolean validateIdentifier(ValidationContext vc, String name) {
44 if (!StandardValidation.notBlank(vc, name)) return false;
45 if (checkForReservedName(vc, name)) return false;
46
47 // Adapted from javax.lang.model.SourceVersion.isIdentifier
48
49 int cp = name.codePointAt(0);
50 int position = 1;
51 if (!Character.isJavaIdentifierStart(cp)) {
52 vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position);
53 return false;
54 }
55 for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
56 cp = name.codePointAt(i);
57 position += 1;
58 if (!Character.isJavaIdentifierPart(cp)) {
59 vc.raise(Message.ILLEGAL_IDENTIFIER, name, new String(Character.toChars(cp)), position);
60 return false;
61 }
62 }
63
64 return true;
65 }
66
67 private static boolean checkForReservedName(ValidationContext vc, String name) {
68 if (isReservedMethodName(name)) {
69 vc.raise(Message.RESERVED_IDENTIFIER);
70 return true;
71 }
72 return false;
73 }
74
75 public static boolean isReservedMethodName(String name) {
76 return ILLEGAL_IDENTIFIERS.contains(name);
77 }
78
79}
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java
deleted file mode 100644
index a7f83cd..0000000
--- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/IllegalNameException.java
+++ /dev/null
@@ -1,39 +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.translation.mapping;
13
14public class IllegalNameException extends RuntimeException {
15
16 private String name;
17 private String reason;
18
19 public IllegalNameException(String name, String reason) {
20 this.name = name;
21 this.reason = reason;
22 }
23
24 public String getReason() {
25 return this.reason;
26 }
27
28 @Override
29 public String getMessage() {
30 StringBuilder buf = new StringBuilder();
31 buf.append("Illegal name: ");
32 buf.append(this.name);
33 if (this.reason != null) {
34 buf.append(" because ");
35 buf.append(this.reason);
36 }
37 return buf.toString();
38 }
39}
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java
index ae615da..f9f3b88 100644
--- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java
+++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/MappingValidator.java
@@ -1,17 +1,20 @@
1package cuchaz.enigma.translation.mapping; 1package cuchaz.enigma.translation.mapping;
2 2
3import java.util.Collection;
4import java.util.HashSet;
5import java.util.stream.Collectors;
6
3import cuchaz.enigma.analysis.index.InheritanceIndex; 7import cuchaz.enigma.analysis.index.InheritanceIndex;
4import cuchaz.enigma.analysis.index.JarIndex; 8import cuchaz.enigma.analysis.index.JarIndex;
5import cuchaz.enigma.translation.Translator; 9import cuchaz.enigma.translation.Translator;
6import cuchaz.enigma.translation.mapping.tree.EntryTree; 10import cuchaz.enigma.translation.mapping.tree.EntryTree;
7import cuchaz.enigma.translation.representation.entry.ClassEntry; 11import cuchaz.enigma.translation.representation.entry.ClassEntry;
8import cuchaz.enigma.translation.representation.entry.Entry; 12import cuchaz.enigma.translation.representation.entry.Entry;
9 13import cuchaz.enigma.utils.validation.Message;
10import java.util.Collection; 14import cuchaz.enigma.utils.validation.ValidationContext;
11import java.util.HashSet;
12import java.util.stream.Collectors;
13 15
14public class MappingValidator { 16public class MappingValidator {
17
15 private final EntryTree<EntryMapping> obfToDeobf; 18 private final EntryTree<EntryMapping> obfToDeobf;
16 private final Translator deobfuscator; 19 private final Translator deobfuscator;
17 private final JarIndex index; 20 private final JarIndex index;
@@ -22,15 +25,15 @@ public class MappingValidator {
22 this.index = index; 25 this.index = index;
23 } 26 }
24 27
25 public void validateRename(Entry<?> entry, String name) throws IllegalNameException { 28 public void validateRename(ValidationContext vc, Entry<?> entry, String name) {
26 Collection<Entry<?>> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry); 29 Collection<Entry<?>> equivalentEntries = index.getEntryResolver().resolveEquivalentEntries(entry);
27 for (Entry<?> equivalentEntry : equivalentEntries) { 30 for (Entry<?> equivalentEntry : equivalentEntries) {
28 equivalentEntry.validateName(name); 31 equivalentEntry.validateName(vc, name);
29 validateUnique(equivalentEntry, name); 32 validateUnique(vc, equivalentEntry, name);
30 } 33 }
31 } 34 }
32 35
33 private void validateUnique(Entry<?> entry, String name) { 36 private void validateUnique(ValidationContext vc, Entry<?> entry, String name) {
34 ClassEntry containingClass = entry.getContainingClass(); 37 ClassEntry containingClass = entry.getContainingClass();
35 Collection<ClassEntry> relatedClasses = getRelatedClasses(containingClass); 38 Collection<ClassEntry> relatedClasses = getRelatedClasses(containingClass);
36 39
@@ -45,9 +48,9 @@ public class MappingValidator {
45 if (!isUnique(translatedEntry, translatedSiblings, name)) { 48 if (!isUnique(translatedEntry, translatedSiblings, name)) {
46 Entry<?> parent = translatedEntry.getParent(); 49 Entry<?> parent = translatedEntry.getParent();
47 if (parent != null) { 50 if (parent != null) {
48 throw new IllegalNameException(name, "Name is not unique in " + parent + "!"); 51 vc.raise(Message.NONUNIQUE_NAME_CLASS, name, parent);
49 } else { 52 } else {
50 throw new IllegalNameException(name, "Name is not unique!"); 53 vc.raise(Message.NONUNIQUE_NAME, name);
51 } 54 }
52 } 55 }
53 } 56 }
@@ -72,4 +75,5 @@ public class MappingValidator {
72 } 75 }
73 return true; 76 return true;
74 } 77 }
78
75} 79}
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java
deleted file mode 100644
index 74ba633..0000000
--- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/NameValidator.java
+++ /dev/null
@@ -1,50 +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.translation.mapping;
13
14import java.util.Arrays;
15import java.util.List;
16import java.util.regex.Pattern;
17
18public class NameValidator {
19 private static final Pattern IDENTIFIER_PATTERN;
20 private static final Pattern CLASS_PATTERN;
21 private static final List<String> ILLEGAL_IDENTIFIERS = Arrays.asList(
22 "abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized",
23 "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte",
24 "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch",
25 "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally",
26 "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "_"
27 );
28
29 static {
30 String identifierRegex = "[A-Za-z_<][A-Za-z0-9_>]*";
31 IDENTIFIER_PATTERN = Pattern.compile(identifierRegex);
32 CLASS_PATTERN = Pattern.compile(String.format("^(%s(\\.|/))*(%s)$", identifierRegex, identifierRegex));
33 }
34
35 public static void validateClassName(String name) {
36 if (!CLASS_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) {
37 throw new IllegalNameException(name, "This doesn't look like a legal class name");
38 }
39 }
40
41 public static void validateIdentifier(String name) {
42 if (!IDENTIFIER_PATTERN.matcher(name).matches() || ILLEGAL_IDENTIFIERS.contains(name)) {
43 throw new IllegalNameException(name, "This doesn't look like a legal identifier");
44 }
45 }
46
47 public static boolean isReserved(String name) {
48 return ILLEGAL_IDENTIFIERS.contains(name);
49 }
50}
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java
index 7d4b2ba..15b0a9b 100644
--- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java
+++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/ClassEntry.java
@@ -11,16 +11,17 @@
11 11
12package cuchaz.enigma.translation.representation.entry; 12package cuchaz.enigma.translation.representation.entry;
13 13
14import cuchaz.enigma.translation.mapping.IllegalNameException; 14import java.util.List;
15import cuchaz.enigma.translation.Translator; 15import java.util.Objects;
16import cuchaz.enigma.translation.mapping.EntryMapping;
17import cuchaz.enigma.translation.mapping.NameValidator;
18import cuchaz.enigma.translation.representation.TypeDescriptor;
19 16
20import javax.annotation.Nonnull; 17import javax.annotation.Nonnull;
21import javax.annotation.Nullable; 18import javax.annotation.Nullable;
22import java.util.List; 19
23import java.util.Objects; 20import cuchaz.enigma.translation.Translator;
21import cuchaz.enigma.translation.mapping.EntryMapping;
22import cuchaz.enigma.translation.mapping.IdentifierValidation;
23import cuchaz.enigma.translation.representation.TypeDescriptor;
24import cuchaz.enigma.utils.validation.ValidationContext;
24 25
25public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<ClassEntry> { 26public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<ClassEntry> {
26 private final String fullName; 27 private final String fullName;
@@ -97,8 +98,8 @@ public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<
97 } 98 }
98 99
99 @Override 100 @Override
100 public void validateName(String name) throws IllegalNameException { 101 public void validateName(ValidationContext vc, String name) {
101 NameValidator.validateClassName(name); 102 IdentifierValidation.validateClassName(vc, name);
102 } 103 }
103 104
104 @Override 105 @Override
diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java
index 40bff31..ff392fe 100644
--- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java
+++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/Entry.java
@@ -11,14 +11,15 @@
11 11
12package cuchaz.enigma.translation.representation.entry; 12package cuchaz.enigma.translation.representation.entry;
13 13
14import cuchaz.enigma.translation.mapping.IllegalNameException;
15import cuchaz.enigma.translation.Translatable;
16import cuchaz.enigma.translation.mapping.NameValidator;
17
18import javax.annotation.Nullable;
19import java.util.ArrayList; 14import java.util.ArrayList;
20import java.util.List; 15import java.util.List;
21 16
17import javax.annotation.Nullable;
18
19import cuchaz.enigma.translation.Translatable;
20import cuchaz.enigma.translation.mapping.IdentifierValidation;
21import cuchaz.enigma.utils.validation.ValidationContext;
22
22public interface Entry<P extends Entry<?>> extends Translatable { 23public interface Entry<P extends Entry<?>> extends Translatable {
23 String getName(); 24 String getName();
24 25
@@ -92,8 +93,8 @@ public interface Entry<P extends Entry<?>> extends Translatable {
92 return withParent((P) parent.replaceAncestor(target, replacement)); 93 return withParent((P) parent.replaceAncestor(target, replacement));
93 } 94 }
94 95
95 default void validateName(String name) throws IllegalNameException { 96 default void validateName(ValidationContext vc, String name) {
96 NameValidator.validateIdentifier(name); 97 IdentifierValidation.validateIdentifier(vc, name);
97 } 98 }
98 99
99 @SuppressWarnings("unchecked") 100 @SuppressWarnings("unchecked")
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/I18n.java b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java
index e18532b..cb498e0 100644
--- a/enigma/src/main/java/cuchaz/enigma/utils/I18n.java
+++ b/enigma/src/main/java/cuchaz/enigma/utils/I18n.java
@@ -5,9 +5,8 @@ import java.io.IOException;
5import java.io.InputStream; 5import java.io.InputStream;
6import java.io.InputStreamReader; 6import java.io.InputStreamReader;
7import java.nio.charset.StandardCharsets; 7import java.nio.charset.StandardCharsets;
8import java.util.ArrayList; 8import java.util.*;
9import java.util.Collections; 9import java.util.stream.Collectors;
10import java.util.Map;
11import java.util.stream.Stream; 10import java.util.stream.Stream;
12 11
13import com.google.common.collect.ImmutableList; 12import com.google.common.collect.ImmutableList;
@@ -22,12 +21,12 @@ public class I18n {
22 private static Map<String, String> translations = Maps.newHashMap(); 21 private static Map<String, String> translations = Maps.newHashMap();
23 private static Map<String, String> defaultTranslations = Maps.newHashMap(); 22 private static Map<String, String> defaultTranslations = Maps.newHashMap();
24 private static Map<String, String> languageNames = Maps.newHashMap(); 23 private static Map<String, String> languageNames = Maps.newHashMap();
25 24
26 static { 25 static {
27 defaultTranslations = load(DEFAULT_LANGUAGE); 26 defaultTranslations = load(DEFAULT_LANGUAGE);
28 translations = defaultTranslations; 27 translations = defaultTranslations;
29 } 28 }
30 29
31 @SuppressWarnings("unchecked") 30 @SuppressWarnings("unchecked")
32 public static Map<String, String> load(String language) { 31 public static Map<String, String> load(String language) {
33 try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) { 32 try (InputStream inputStream = I18n.class.getResourceAsStream("/lang/" + language + ".json")) {
@@ -41,30 +40,50 @@ public class I18n {
41 } 40 }
42 return Collections.emptyMap(); 41 return Collections.emptyMap();
43 } 42 }
44 43
45 public static String translate(String key) { 44 public static String translateOrNull(String key) {
46 String value = translations.get(key); 45 String value = translations.get(key);
47 if (value != null) { 46 if (value != null) return value;
48 return value; 47
48 return defaultTranslations.get(key);
49 }
50
51 public static String translate(String key) {
52 String tr = translateOrNull(key);
53 return tr != null ? tr : key;
54 }
55
56 public static String translateOrEmpty(String key, Object... args) {
57 String text = translateOrNull(key);
58 if (text != null) {
59 return String.format(text, args);
60 } else {
61 return "";
49 } 62 }
50 value = defaultTranslations.get(key); 63 }
51 if (value != null) { 64
52 return value; 65 public static String translateFormatted(String key, Object... args) {
66 String text = translateOrNull(key);
67 if (text != null) {
68 return String.format(text, args);
69 } else if (args.length == 0) {
70 return key;
71 } else {
72 return key + Arrays.stream(args).map(Objects::toString).collect(Collectors.joining(", ", "[", "]"));
53 } 73 }
54 return key;
55 } 74 }
56 75
57 public static String getLanguageName(String language) { 76 public static String getLanguageName(String language) {
58 return languageNames.get(language); 77 return languageNames.get(language);
59 } 78 }
60 79
61 public static void setLanguage(String language) { 80 public static void setLanguage(String language) {
62 translations = load(language); 81 translations = load(language);
63 } 82 }
64 83
65 public static ArrayList<String> getAvailableLanguages() { 84 public static ArrayList<String> getAvailableLanguages() {
66 ArrayList<String> list = new ArrayList<String>(); 85 ArrayList<String> list = new ArrayList<String>();
67 86
68 try { 87 try {
69 ImmutableList<ResourceInfo> resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList(); 88 ImmutableList<ResourceInfo> resources = ClassPath.from(Thread.currentThread().getContextClassLoader()).getResources().asList();
70 Stream<ResourceInfo> dirStream = resources.stream(); 89 Stream<ResourceInfo> dirStream = resources.stream();
@@ -81,7 +100,7 @@ public class I18n {
81 } 100 }
82 return list; 101 return list;
83 } 102 }
84 103
85 private static void loadLanguageName(String fileName) { 104 private static void loadLanguageName(String fileName) {
86 try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) { 105 try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lang/" + fileName + ".json")) {
87 try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { 106 try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Result.java b/enigma/src/main/java/cuchaz/enigma/utils/Result.java
new file mode 100644
index 0000000..dcaabd5
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/Result.java
@@ -0,0 +1,108 @@
1package cuchaz.enigma.utils;
2
3import java.util.Objects;
4import java.util.Optional;
5import java.util.function.Function;
6
7public final class Result<T, E> {
8
9 private final T ok;
10 private final E err;
11
12 private Result(T ok, E err) {
13 this.ok = ok;
14 this.err = err;
15 }
16
17 public static <T, E> Result<T, E> ok(T ok) {
18 return new Result<>(Objects.requireNonNull(ok), null);
19 }
20
21 public static <T, E> Result<T, E> err(E err) {
22 return new Result<>(null, Objects.requireNonNull(err));
23 }
24
25 public boolean isOk() {
26 return this.ok != null;
27 }
28
29 public boolean isErr() {
30 return this.err != null;
31 }
32
33 public Optional<T> ok() {
34 return Optional.ofNullable(this.ok);
35 }
36
37 public Optional<E> err() {
38 return Optional.ofNullable(this.err);
39 }
40
41 public T unwrap() {
42 if (this.isOk()) return this.ok;
43 throw new IllegalStateException(String.format("Called Result.unwrap on an Err value: %s", this.err));
44 }
45
46 public E unwrapErr() {
47 if (this.isErr()) return this.err;
48 throw new IllegalStateException(String.format("Called Result.unwrapErr on an Ok value: %s", this.ok));
49 }
50
51 public T unwrapOr(T fallback) {
52 if (this.isOk()) return this.ok;
53 return fallback;
54 }
55
56 public T unwrapOrElse(Function<E, T> fn) {
57 if (this.isOk()) return this.ok;
58 return fn.apply(this.err);
59 }
60
61 @SuppressWarnings("unchecked")
62 public <U> Result<U, E> map(Function<T, U> op) {
63 if (!this.isOk()) return (Result<U, E>) this;
64 return Result.ok(op.apply(this.ok));
65 }
66
67 @SuppressWarnings("unchecked")
68 public <F> Result<T, F> mapErr(Function<E, F> op) {
69 if (!this.isErr()) return (Result<T, F>) this;
70 return Result.err(op.apply(this.err));
71 }
72
73 @SuppressWarnings("unchecked")
74 public <U> Result<U, E> and(Result<U, E> next) {
75 if (this.isErr()) return (Result<U, E>) this;
76 return next;
77 }
78
79 @SuppressWarnings("unchecked")
80 public <U> Result<U, E> andThen(Function<T, Result<U, E>> op) {
81 if (this.isErr()) return (Result<U, E>) this;
82 return op.apply(this.ok);
83 }
84
85 @Override
86 public boolean equals(Object o) {
87 if (this == o) return true;
88 if (o == null || getClass() != o.getClass()) return false;
89 Result<?, ?> result = (Result<?, ?>) o;
90 return Objects.equals(ok, result.ok) &&
91 Objects.equals(err, result.err);
92 }
93
94 @Override
95 public int hashCode() {
96 return Objects.hash(ok, err);
97 }
98
99 @Override
100 public String toString() {
101 if (this.isOk()) {
102 return String.format("Result.Ok(%s)", this.ok);
103 } else {
104 return String.format("Result.Err(%s)", this.err);
105 }
106 }
107
108}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Utils.java b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java
index 2664099..8beaaae 100644
--- a/enigma/src/main/java/cuchaz/enigma/utils/Utils.java
+++ b/enigma/src/main/java/cuchaz/enigma/utils/Utils.java
@@ -25,6 +25,8 @@ import java.util.Collections;
25import java.util.Comparator; 25import java.util.Comparator;
26import java.util.List; 26import java.util.List;
27import java.util.Locale; 27import java.util.Locale;
28import java.util.concurrent.locks.Lock;
29import java.util.function.Supplier;
28import java.util.stream.Collectors; 30import java.util.stream.Collectors;
29import java.util.zip.ZipEntry; 31import java.util.zip.ZipEntry;
30import java.util.zip.ZipFile; 32import java.util.zip.ZipFile;
@@ -78,6 +80,25 @@ public class Utils {
78 return digest.digest(); 80 return digest.digest();
79 } 81 }
80 82
83 public static void withLock(Lock l, Runnable op) {
84 try {
85 l.lock();
86 op.run();
87 } finally {
88 l.unlock();
89 }
90 }
91
92 public static <R> R withLock(Lock l, Supplier<R> op) {
93 try {
94 l.lock();
95 return op.get();
96 } finally {
97 l.unlock();
98 }
99 }
100
101
81 public static boolean isBlank(String input) { 102 public static boolean isBlank(String input) {
82 if (input == null) { 103 if (input == null) {
83 return true; 104 return true;
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java
new file mode 100644
index 0000000..dca74bc
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/Message.java
@@ -0,0 +1,48 @@
1package cuchaz.enigma.utils.validation;
2
3import cuchaz.enigma.utils.I18n;
4
5public class Message {
6
7 public static final Message EMPTY_FIELD = create(Type.ERROR, "empty_field");
8 public static final Message NOT_INT = create(Type.ERROR, "not_int");
9 public static final Message FIELD_OUT_OF_RANGE_INT = create(Type.ERROR, "field_out_of_range_int");
10 public static final Message FIELD_LENGTH_OUT_OF_RANGE = create(Type.ERROR, "field_length_out_of_range");
11 public static final Message NONUNIQUE_NAME_CLASS = create(Type.ERROR, "nonunique_name_class");
12 public static final Message NONUNIQUE_NAME = create(Type.ERROR, "nonunique_name");
13 public static final Message ILLEGAL_CLASS_NAME = create(Type.ERROR, "illegal_class_name");
14 public static final Message ILLEGAL_IDENTIFIER = create(Type.ERROR, "illegal_identifier");
15 public static final Message RESERVED_IDENTIFIER = create(Type.ERROR, "reserved_identifier");
16 public static final Message ILLEGAL_DOC_COMMENT_END = create(Type.ERROR, "illegal_doc_comment_end");
17
18 public static final Message STYLE_VIOLATION = create(Type.WARNING, "style_violation");
19
20 public final Type type;
21 public final String textKey;
22 public final String longTextKey;
23
24 private Message(Type type, String textKey, String longTextKey) {
25 this.type = type;
26 this.textKey = textKey;
27 this.longTextKey = longTextKey;
28 }
29
30 public String format(Object[] args) {
31 return I18n.translateFormatted(textKey, args);
32 }
33
34 public String formatDetails(Object[] args) {
35 return I18n.translateOrEmpty(longTextKey, args);
36 }
37
38 public static Message create(Type type, String name) {
39 return new Message(type, String.format("validation.message.%s", name), String.format("validation.message.%s.long", name));
40 }
41
42 public enum Type {
43 INFO,
44 WARNING,
45 ERROR,
46 }
47
48}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java
new file mode 100644
index 0000000..56b0ecc
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/ParameterizedMessage.java
@@ -0,0 +1,45 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.Arrays;
4import java.util.Objects;
5
6public class ParameterizedMessage {
7
8 public final Message message;
9 private final Object[] params;
10
11 public ParameterizedMessage(Message message, Object[] params) {
12 this.message = message;
13 this.params = params;
14 }
15
16 public String getText() {
17 return message.format(params);
18 }
19
20 public String getLongText() {
21 return message.formatDetails(params);
22 }
23
24 @Override
25 public boolean equals(Object o) {
26 if (this == o) return true;
27 if (o == null || getClass() != o.getClass()) return false;
28 ParameterizedMessage that = (ParameterizedMessage) o;
29 return Objects.equals(message, that.message) &&
30 Arrays.equals(params, that.params);
31 }
32
33 @Override
34 public int hashCode() {
35 int result = Objects.hash(message);
36 result = 31 * result + Arrays.hashCode(params);
37 return result;
38 }
39
40 @Override
41 public String toString() {
42 return String.format("ParameterizedMessage { message: %s, params: %s }", message, Arrays.toString(params));
43 }
44
45}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java
new file mode 100644
index 0000000..fe91cc1
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/PrintValidatable.java
@@ -0,0 +1,37 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.Arrays;
4
5public class PrintValidatable implements Validatable {
6
7 public static final PrintValidatable INSTANCE = new PrintValidatable();
8
9 @Override
10 public void addMessage(ParameterizedMessage message) {
11 String text = message.getText();
12 String longText = message.getLongText();
13 String type;
14 switch (message.message.type) {
15 case INFO:
16 type = "info";
17 break;
18 case WARNING:
19 type = "warning";
20 break;
21 case ERROR:
22 type = "error";
23 break;
24 default:
25 throw new IllegalStateException("unreachable");
26 }
27 System.out.printf("%s: %s\n", type, text);
28 if (!longText.isEmpty()) {
29 Arrays.stream(longText.split("\n")).forEach(s -> System.out.printf(" %s\n", s));
30 }
31 }
32
33 @Override
34 public void clearMessages() {
35 }
36
37}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java
new file mode 100644
index 0000000..871b59d
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/StandardValidation.java
@@ -0,0 +1,34 @@
1package cuchaz.enigma.utils.validation;
2
3public class StandardValidation {
4
5 public static boolean notBlank(ValidationContext vc, String value) {
6 if (value.trim().isEmpty()) {
7 vc.raise(Message.EMPTY_FIELD);
8 return false;
9 }
10 return true;
11 }
12
13 public static boolean isInt(ValidationContext vc, String value) {
14 if (!notBlank(vc, value)) return false;
15 try {
16 Integer.parseInt(value);
17 return true;
18 } catch (NumberFormatException e) {
19 vc.raise(Message.NOT_INT);
20 return false;
21 }
22 }
23
24 public static boolean isIntInRange(ValidationContext vc, String value, int min, int max) {
25 if (!isInt(vc, value)) return false;
26 int intVal = Integer.parseInt(value);
27 if (intVal < min || intVal > max) {
28 vc.raise(Message.FIELD_OUT_OF_RANGE_INT, min, max);
29 return false;
30 }
31 return true;
32 }
33
34}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java
new file mode 100644
index 0000000..765ee08
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/Validatable.java
@@ -0,0 +1,9 @@
1package cuchaz.enigma.utils.validation;
2
3public interface Validatable {
4
5 void addMessage(ParameterizedMessage message);
6
7 void clearMessages();
8
9}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java b/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java
new file mode 100644
index 0000000..d38fc21
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/validation/ValidationContext.java
@@ -0,0 +1,78 @@
1package cuchaz.enigma.utils.validation;
2
3import java.util.*;
4
5import javax.annotation.Nullable;
6
7import cuchaz.enigma.utils.validation.Message.Type;
8
9/**
10 * A context for user input validation. Handles collecting error messages and
11 * displaying the errors on the relevant input fields. UIs using validation
12 * often have two stages of applying changes: validating all the input fields,
13 * then checking if there's any errors or unconfirmed warnings, and if not,
14 * then actually applying the changes. This allows for easily collecting
15 * multiple errors and displaying them to the user at the same time.
16 */
17public class ValidationContext {
18
19 private Validatable activeElement = null;
20 private final Set<Validatable> elements = new HashSet<>();
21 private final List<ParameterizedMessage> messages = new ArrayList<>();
22
23 /**
24 * Sets the currently active element (such as an input field). Any messages
25 * raised while this is set get displayed on this element.
26 *
27 * @param v the active element to set, or {@code null} to unset
28 */
29 public void setActiveElement(@Nullable Validatable v) {
30 if (v != null) {
31 elements.add(v);
32 }
33 activeElement = v;
34 }
35
36 /**
37 * Raises a message. If there's a currently active element, also notifies
38 * that element about the message.
39 *
40 * @param message the message to raise
41 * @param args the arguments used when formatting the message text
42 */
43 public void raise(Message message, Object... args) {
44 ParameterizedMessage pm = new ParameterizedMessage(message, args);
45 if (activeElement != null) {
46 activeElement.addMessage(pm);
47 }
48 messages.add(pm);
49 }
50
51 /**
52 * Returns whether the validation context currently has no messages that
53 * block executing actions, such as errors and unconfirmed warnings.
54 *
55 * @return whether the program can proceed executing and the UI is in a
56 * valid state
57 */
58 public boolean canProceed() {
59 // TODO on warnings, wait until user confirms
60 return messages.stream().noneMatch(m -> m.message.type == Type.ERROR);
61 }
62
63 public List<ParameterizedMessage> getMessages() {
64 return Collections.unmodifiableList(messages);
65 }
66
67 /**
68 * Clears all currently pending messages. This should be called whenever the
69 * interface starts getting validated, to get rid of old messages.
70 */
71 public void reset() {
72 activeElement = null;
73 elements.forEach(Validatable::clearMessages);
74 elements.clear();
75 messages.clear();
76 }
77
78}