diff options
| author | 2020-06-03 20:16:10 +0200 | |
|---|---|---|
| committer | 2020-06-03 19:16:10 +0100 | |
| commit | 5a286d58e740f1aa5944488c602f5abc1318f6ca (patch) | |
| tree | dfde9eff0c744906b3571390af0f6a6e3be92a91 /enigma/src | |
| parent | Refactor MenuBar (#251) (diff) | |
| download | enigma-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')
26 files changed, 1395 insertions, 155 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 | ||
| 12 | package cuchaz.enigma.analysis; | 12 | package cuchaz.enigma.analysis; |
| 13 | 13 | ||
| 14 | import java.util.Arrays; | ||
| 15 | import java.util.List; | ||
| 16 | import java.util.Objects; | ||
| 17 | |||
| 14 | import cuchaz.enigma.translation.Translatable; | 18 | import cuchaz.enigma.translation.Translatable; |
| 15 | import cuchaz.enigma.translation.Translator; | 19 | import cuchaz.enigma.translation.Translator; |
| 20 | import cuchaz.enigma.translation.mapping.EntryMap; | ||
| 16 | import cuchaz.enigma.translation.mapping.EntryMapping; | 21 | import cuchaz.enigma.translation.mapping.EntryMapping; |
| 17 | import cuchaz.enigma.translation.mapping.EntryResolver; | 22 | import cuchaz.enigma.translation.mapping.EntryResolver; |
| 18 | import cuchaz.enigma.translation.mapping.EntryMap; | ||
| 19 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 23 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 20 | import cuchaz.enigma.translation.representation.entry.Entry; | 24 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 21 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | 25 | import cuchaz.enigma.translation.representation.entry.MethodEntry; |
| 22 | 26 | ||
| 23 | import java.util.Arrays; | ||
| 24 | import java.util.List; | ||
| 25 | import java.util.Objects; | ||
| 26 | |||
| 27 | public class EntryReference<E extends Entry<?>, C extends Entry<?>> implements Translatable { | 27 | public 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 @@ | |||
| 1 | package cuchaz.enigma.classhandle; | ||
| 2 | |||
| 3 | import java.util.concurrent.CompletableFuture; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | |||
| 7 | import cuchaz.enigma.events.ClassHandleListener; | ||
| 8 | import cuchaz.enigma.source.DecompiledClassSource; | ||
| 9 | import cuchaz.enigma.source.Source; | ||
| 10 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 11 | import 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 | */ | ||
| 20 | public 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 @@ | |||
| 1 | package cuchaz.enigma.classhandle; | ||
| 2 | |||
| 3 | import java.io.ByteArrayOutputStream; | ||
| 4 | import java.io.PrintStream; | ||
| 5 | |||
| 6 | import javax.annotation.Nullable; | ||
| 7 | |||
| 8 | public 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 @@ | |||
| 1 | package cuchaz.enigma.classhandle; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | import java.util.concurrent.CompletableFuture; | ||
| 5 | import java.util.concurrent.ExecutorService; | ||
| 6 | import java.util.concurrent.Executors; | ||
| 7 | import java.util.concurrent.TimeUnit; | ||
| 8 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 9 | import java.util.concurrent.locks.ReadWriteLock; | ||
| 10 | import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
| 11 | |||
| 12 | import javax.annotation.Nullable; | ||
| 13 | |||
| 14 | import cuchaz.enigma.Enigma; | ||
| 15 | import cuchaz.enigma.EnigmaProject; | ||
| 16 | import cuchaz.enigma.bytecode.translators.SourceFixVisitor; | ||
| 17 | import cuchaz.enigma.events.ClassHandleListener; | ||
| 18 | import cuchaz.enigma.events.ClassHandleListener.InvalidationType; | ||
| 19 | import cuchaz.enigma.source.*; | ||
| 20 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 21 | import cuchaz.enigma.utils.Result; | ||
| 22 | import org.objectweb.asm.tree.ClassNode; | ||
| 23 | |||
| 24 | import static cuchaz.enigma.utils.Utils.withLock; | ||
| 25 | |||
| 26 | public 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 @@ | |||
| 1 | package cuchaz.enigma.events; | ||
| 2 | |||
| 3 | import cuchaz.enigma.classhandle.ClassHandle; | ||
| 4 | import cuchaz.enigma.classhandle.ClassHandleError; | ||
| 5 | import cuchaz.enigma.source.DecompiledClassSource; | ||
| 6 | import cuchaz.enigma.source.Source; | ||
| 7 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 8 | import cuchaz.enigma.utils.Result; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.source; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | |||
| 7 | import cuchaz.enigma.EnigmaProject; | ||
| 8 | import cuchaz.enigma.EnigmaServices; | ||
| 9 | import cuchaz.enigma.analysis.EntryReference; | ||
| 10 | import cuchaz.enigma.api.service.NameProposalService; | ||
| 11 | import cuchaz.enigma.translation.LocalNameGenerator; | ||
| 12 | import cuchaz.enigma.translation.Translator; | ||
| 13 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 14 | import cuchaz.enigma.translation.mapping.ResolutionStrategy; | ||
| 15 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 16 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 17 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 18 | import cuchaz.enigma.translation.representation.entry.LocalVariableDefEntry; | ||
| 19 | |||
| 20 | public 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 @@ | |||
| 1 | package cuchaz.enigma.source; | ||
| 2 | |||
| 3 | public 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 @@ | |||
| 1 | package cuchaz.enigma.translation; | 1 | package cuchaz.enigma.translation; |
| 2 | 2 | ||
| 3 | import cuchaz.enigma.translation.mapping.NameValidator; | ||
| 4 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 5 | |||
| 6 | import java.util.Collection; | 3 | import java.util.Collection; |
| 7 | import java.util.Locale; | 4 | import java.util.Locale; |
| 8 | 5 | ||
| 6 | import cuchaz.enigma.translation.mapping.IdentifierValidation; | ||
| 7 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 8 | |||
| 9 | public class LocalNameGenerator { | 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | 1 | package cuchaz.enigma.translation.mapping; |
| 2 | 2 | ||
| 3 | import java.util.Collection; | ||
| 4 | import java.util.stream.Stream; | ||
| 5 | |||
| 6 | import javax.annotation.Nullable; | ||
| 7 | |||
| 3 | import cuchaz.enigma.analysis.index.JarIndex; | 8 | import cuchaz.enigma.analysis.index.JarIndex; |
| 4 | import cuchaz.enigma.translation.MappingTranslator; | 9 | import cuchaz.enigma.translation.MappingTranslator; |
| 5 | import cuchaz.enigma.translation.Translatable; | 10 | import cuchaz.enigma.translation.Translatable; |
| @@ -8,10 +13,7 @@ import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; | |||
| 8 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | 13 | import cuchaz.enigma.translation.mapping.tree.EntryTree; |
| 9 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | 14 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; |
| 10 | import cuchaz.enigma.translation.representation.entry.Entry; | 15 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 11 | 16 | import cuchaz.enigma.utils.validation.ValidationContext; | |
| 12 | import javax.annotation.Nullable; | ||
| 13 | import java.util.Collection; | ||
| 14 | import java.util.stream.Stream; | ||
| 15 | 17 | ||
| 16 | public class EntryRemapper { | 18 | public 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 | |||
| 12 | package cuchaz.enigma.translation.mapping; | ||
| 13 | |||
| 14 | import java.util.Arrays; | ||
| 15 | import java.util.List; | ||
| 16 | |||
| 17 | import cuchaz.enigma.utils.validation.Message; | ||
| 18 | import cuchaz.enigma.utils.validation.StandardValidation; | ||
| 19 | import cuchaz.enigma.utils.validation.ValidationContext; | ||
| 20 | |||
| 21 | public 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 | |||
| 12 | package cuchaz.enigma.translation.mapping; | ||
| 13 | |||
| 14 | public 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 @@ | |||
| 1 | package cuchaz.enigma.translation.mapping; | 1 | package cuchaz.enigma.translation.mapping; |
| 2 | 2 | ||
| 3 | import java.util.Collection; | ||
| 4 | import java.util.HashSet; | ||
| 5 | import java.util.stream.Collectors; | ||
| 6 | |||
| 3 | import cuchaz.enigma.analysis.index.InheritanceIndex; | 7 | import cuchaz.enigma.analysis.index.InheritanceIndex; |
| 4 | import cuchaz.enigma.analysis.index.JarIndex; | 8 | import cuchaz.enigma.analysis.index.JarIndex; |
| 5 | import cuchaz.enigma.translation.Translator; | 9 | import cuchaz.enigma.translation.Translator; |
| 6 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | 10 | import cuchaz.enigma.translation.mapping.tree.EntryTree; |
| 7 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 11 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 8 | import cuchaz.enigma.translation.representation.entry.Entry; | 12 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 9 | 13 | import cuchaz.enigma.utils.validation.Message; | |
| 10 | import java.util.Collection; | 14 | import cuchaz.enigma.utils.validation.ValidationContext; |
| 11 | import java.util.HashSet; | ||
| 12 | import java.util.stream.Collectors; | ||
| 13 | 15 | ||
| 14 | public class MappingValidator { | 16 | public 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 | |||
| 12 | package cuchaz.enigma.translation.mapping; | ||
| 13 | |||
| 14 | import java.util.Arrays; | ||
| 15 | import java.util.List; | ||
| 16 | import java.util.regex.Pattern; | ||
| 17 | |||
| 18 | public 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 | ||
| 12 | package cuchaz.enigma.translation.representation.entry; | 12 | package cuchaz.enigma.translation.representation.entry; |
| 13 | 13 | ||
| 14 | import cuchaz.enigma.translation.mapping.IllegalNameException; | 14 | import java.util.List; |
| 15 | import cuchaz.enigma.translation.Translator; | 15 | import java.util.Objects; |
| 16 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 17 | import cuchaz.enigma.translation.mapping.NameValidator; | ||
| 18 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 19 | 16 | ||
| 20 | import javax.annotation.Nonnull; | 17 | import javax.annotation.Nonnull; |
| 21 | import javax.annotation.Nullable; | 18 | import javax.annotation.Nullable; |
| 22 | import java.util.List; | 19 | |
| 23 | import java.util.Objects; | 20 | import cuchaz.enigma.translation.Translator; |
| 21 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 22 | import cuchaz.enigma.translation.mapping.IdentifierValidation; | ||
| 23 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 24 | import cuchaz.enigma.utils.validation.ValidationContext; | ||
| 24 | 25 | ||
| 25 | public class ClassEntry extends ParentedEntry<ClassEntry> implements Comparable<ClassEntry> { | 26 | public 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 | ||
| 12 | package cuchaz.enigma.translation.representation.entry; | 12 | package cuchaz.enigma.translation.representation.entry; |
| 13 | 13 | ||
| 14 | import cuchaz.enigma.translation.mapping.IllegalNameException; | ||
| 15 | import cuchaz.enigma.translation.Translatable; | ||
| 16 | import cuchaz.enigma.translation.mapping.NameValidator; | ||
| 17 | |||
| 18 | import javax.annotation.Nullable; | ||
| 19 | import java.util.ArrayList; | 14 | import java.util.ArrayList; |
| 20 | import java.util.List; | 15 | import java.util.List; |
| 21 | 16 | ||
| 17 | import javax.annotation.Nullable; | ||
| 18 | |||
| 19 | import cuchaz.enigma.translation.Translatable; | ||
| 20 | import cuchaz.enigma.translation.mapping.IdentifierValidation; | ||
| 21 | import cuchaz.enigma.utils.validation.ValidationContext; | ||
| 22 | |||
| 22 | public interface Entry<P extends Entry<?>> extends Translatable { | 23 | public 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; | |||
| 5 | import java.io.InputStream; | 5 | import java.io.InputStream; |
| 6 | import java.io.InputStreamReader; | 6 | import java.io.InputStreamReader; |
| 7 | import java.nio.charset.StandardCharsets; | 7 | import java.nio.charset.StandardCharsets; |
| 8 | import java.util.ArrayList; | 8 | import java.util.*; |
| 9 | import java.util.Collections; | 9 | import java.util.stream.Collectors; |
| 10 | import java.util.Map; | ||
| 11 | import java.util.stream.Stream; | 10 | import java.util.stream.Stream; |
| 12 | 11 | ||
| 13 | import com.google.common.collect.ImmutableList; | 12 | import 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 @@ | |||
| 1 | package cuchaz.enigma.utils; | ||
| 2 | |||
| 3 | import java.util.Objects; | ||
| 4 | import java.util.Optional; | ||
| 5 | import java.util.function.Function; | ||
| 6 | |||
| 7 | public 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; | |||
| 25 | import java.util.Comparator; | 25 | import java.util.Comparator; |
| 26 | import java.util.List; | 26 | import java.util.List; |
| 27 | import java.util.Locale; | 27 | import java.util.Locale; |
| 28 | import java.util.concurrent.locks.Lock; | ||
| 29 | import java.util.function.Supplier; | ||
| 28 | import java.util.stream.Collectors; | 30 | import java.util.stream.Collectors; |
| 29 | import java.util.zip.ZipEntry; | 31 | import java.util.zip.ZipEntry; |
| 30 | import java.util.zip.ZipFile; | 32 | import 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | import cuchaz.enigma.utils.I18n; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | import java.util.Arrays; | ||
| 4 | import java.util.Objects; | ||
| 5 | |||
| 6 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | import java.util.Arrays; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils.validation; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | |||
| 5 | import javax.annotation.Nullable; | ||
| 6 | |||
| 7 | import 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 | */ | ||
| 17 | public 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 | } | ||
diff --git a/enigma/src/main/resources/lang/de_de.json b/enigma/src/main/resources/lang/de_de.json new file mode 100644 index 0000000..ef41da1 --- /dev/null +++ b/enigma/src/main/resources/lang/de_de.json | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | { | ||
| 2 | "language": "German", | ||
| 3 | |||
| 4 | "general.retry": "Wiederholen", | ||
| 5 | |||
| 6 | "popup_menu.editor_tab.close": "Schließen", | ||
| 7 | "popup_menu.editor_tab.close_all": "Alle schließen", | ||
| 8 | "popup_menu.editor_tab.close_others": "Andere schließen", | ||
| 9 | "popup_menu.editor_tab.close_left": "Alle links hiervon schließen", | ||
| 10 | "popup_menu.editor_tab.close_right": "Alle rechts hiervon schließen", | ||
| 11 | |||
| 12 | "editor.decompiling": "Dekompiliere...", | ||
| 13 | "editor.decompile_error": "Ein Fehler ist während des Dekompilierens aufgetreten.", | ||
| 14 | |||
| 15 | "validation.message.empty_field": "Dieses Feld muss ausgefüllt werden.", | ||
| 16 | "validation.message.not_int": "Wert muss eine ganze Zahl sein.", | ||
| 17 | "validation.message.field_out_of_range_int": "Wert muss eine ganze Zahl zwischen %d und %d sein.", | ||
| 18 | "validation.message.field_length_out_of_range": "Wert muss kürzer als %d Zeichen sein.", | ||
| 19 | "validation.message.nonunique_name_class": "Name „%s“ ist in „%s“ nicht eindeutig.", | ||
| 20 | "validation.message.nonunique_name": "Name „%s“ ist nicht eindeutig.", | ||
| 21 | "validation.message.illegal_class_name": "„%s“ ist kein gültiger Klassenname.", | ||
| 22 | "validation.message.illegal_identifier": "„%s“ ist kein gültiger Name.", | ||
| 23 | "validation.message.illegal_identifier.long": "Ungültiges Zeichen „%2$s“ an Position %3$d.", | ||
| 24 | "validation.message.illegal_doc_comment_end": "Javadoc-Kommentar darf die Zeichenfolge „*/“ nicht enthalten.", | ||
| 25 | "validation.message.reserved_identifier": "„%s“ ist ein reservierter Name." | ||
| 26 | } \ No newline at end of file | ||
diff --git a/enigma/src/main/resources/lang/en_us.json b/enigma/src/main/resources/lang/en_us.json index dbf4b93..b8db4b0 100644 --- a/enigma/src/main/resources/lang/en_us.json +++ b/enigma/src/main/resources/lang/en_us.json | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | { | 1 | { |
| 2 | "language": "English", | 2 | "language": "English", |
| 3 | 3 | ||
| 4 | "general.retry": "Retry", | ||
| 5 | |||
| 4 | "mapping_format.enigma_file": "Enigma File", | 6 | "mapping_format.enigma_file": "Enigma File", |
| 5 | "mapping_format.enigma_directory": "Enigma Directory", | 7 | "mapping_format.enigma_directory": "Enigma Directory", |
| 6 | "mapping_format.enigma_zip": "Enigma ZIP", | 8 | "mapping_format.enigma_zip": "Enigma ZIP", |
| @@ -69,6 +71,14 @@ | |||
| 69 | "popup_menu.zoom.in": "Zoom in", | 71 | "popup_menu.zoom.in": "Zoom in", |
| 70 | "popup_menu.zoom.out": "Zoom out", | 72 | "popup_menu.zoom.out": "Zoom out", |
| 71 | "popup_menu.zoom.reset": "Reset zoom", | 73 | "popup_menu.zoom.reset": "Reset zoom", |
| 74 | "popup_menu.editor_tab.close": "Close", | ||
| 75 | "popup_menu.editor_tab.close_all": "Close All", | ||
| 76 | "popup_menu.editor_tab.close_others": "Close Others", | ||
| 77 | "popup_menu.editor_tab.close_left": "Close All to the Left", | ||
| 78 | "popup_menu.editor_tab.close_right": "Close All to the Right", | ||
| 79 | |||
| 80 | "editor.decompiling": "Decompiling...", | ||
| 81 | "editor.decompile_error": "An error was encountered while decompiling.", | ||
| 72 | 82 | ||
| 73 | "info_panel.classes.obfuscated": "Obfuscated Classes", | 83 | "info_panel.classes.obfuscated": "Obfuscated Classes", |
| 74 | "info_panel.classes.deobfuscated": "De-obfuscated Classes", | 84 | "info_panel.classes.deobfuscated": "De-obfuscated Classes", |
| @@ -79,8 +89,8 @@ | |||
| 79 | "info_panel.identifier.method": "Method", | 89 | "info_panel.identifier.method": "Method", |
| 80 | "info_panel.identifier.constructor": "Constructor", | 90 | "info_panel.identifier.constructor": "Constructor", |
| 81 | "info_panel.identifier.class": "Class", | 91 | "info_panel.identifier.class": "Class", |
| 82 | "info_panel.identifier.type_descriptor": "TypeDescriptor", | 92 | "info_panel.identifier.type_descriptor": "Type Descriptor", |
| 83 | "info_panel.identifier.method_descriptor": "MethodDescriptor", | 93 | "info_panel.identifier.method_descriptor": "Method Descriptor", |
| 84 | "info_panel.identifier.modifier": "Modifier", | 94 | "info_panel.identifier.modifier": "Modifier", |
| 85 | "info_panel.identifier.index": "Index", | 95 | "info_panel.identifier.index": "Index", |
| 86 | "info_panel.editor.class.decompiling": "(decompiling...)", | 96 | "info_panel.editor.class.decompiling": "(decompiling...)", |
| @@ -149,12 +159,23 @@ | |||
| 149 | "message.mark_deobf.text": "%s marked %s as deobfuscated", | 159 | "message.mark_deobf.text": "%s marked %s as deobfuscated", |
| 150 | "message.remove_mapping.text": "%s removed mappings for %s", | 160 | "message.remove_mapping.text": "%s removed mappings for %s", |
| 151 | "message.rename.text": "%s renamed %s to %s", | 161 | "message.rename.text": "%s renamed %s to %s", |
| 152 | |||
| 153 | "status.disconnected": "Disconnected.", | 162 | "status.disconnected": "Disconnected.", |
| 154 | "status.connected": "Connected.", | 163 | "status.connected": "Connected.", |
| 155 | "status.connected_user_count": "Connected (%d users).", | 164 | "status.connected_user_count": "Connected (%d users).", |
| 156 | "status.ready": "Ready.", | 165 | "status.ready": "Ready.", |
| 157 | 166 | ||
| 167 | "validation.message.empty_field": "This field is required.", | ||
| 168 | "validation.message.not_int": "Value must be an integer.", | ||
| 169 | "validation.message.field_out_of_range_int": "Value must be an integer between %d and %d.", | ||
| 170 | "validation.message.field_length_out_of_range": "Value must be less than %d characters long.", | ||
| 171 | "validation.message.nonunique_name_class": "Name '%s' is not unique in '%s'.", | ||
| 172 | "validation.message.nonunique_name": "Name '%s' is not unique.", | ||
| 173 | "validation.message.illegal_class_name": "'%s' is not a valid class name.", | ||
| 174 | "validation.message.illegal_identifier": "'%s' is not a valid identifier.", | ||
| 175 | "validation.message.illegal_identifier.long": "Invalid character '%2$s' at position %3$d.", | ||
| 176 | "validation.message.illegal_doc_comment_end": "Javadoc comment cannot contain the character sequence '*/'.", | ||
| 177 | "validation.message.reserved_identifier": "'%s' is a reserved identifier.", | ||
| 178 | |||
| 158 | "crash.title": "%s - Crash Report", | 179 | "crash.title": "%s - Crash Report", |
| 159 | "crash.summary": "%s has crashed! =(", | 180 | "crash.summary": "%s has crashed! =(", |
| 160 | "crash.export": "Export", | 181 | "crash.export": "Export", |