/*
* Nocturne
* Copyright (c) 2015-2016, Lapis <https://github.com/LapisBlue>
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package blue.lapis.nocturne.gui.scene.text;
import static blue.lapis.nocturne.util.Constants.CLASS_PATH_SEPARATOR_CHAR;
import static blue.lapis.nocturne.util.Constants.DOT_PATTERN;
import static blue.lapis.nocturne.util.Constants.INNER_CLASS_SEPARATOR_CHAR;
import static blue.lapis.nocturne.util.Constants.Processing.CLASS_PREFIX;
import static blue.lapis.nocturne.util.helper.MappingsHelper.genMethodMapping;
import static blue.lapis.nocturne.util.helper.MappingsHelper.getOrCreateClassMapping;
import blue.lapis.nocturne.Main;
import blue.lapis.nocturne.gui.MainController;
import blue.lapis.nocturne.gui.scene.control.CodeTab;
import blue.lapis.nocturne.jar.model.JarClassEntry;
import blue.lapis.nocturne.jar.model.attribute.MethodDescriptor;
import blue.lapis.nocturne.jar.model.attribute.Type;
import blue.lapis.nocturne.mapping.model.ClassMapping;
import blue.lapis.nocturne.mapping.model.FieldMapping;
import blue.lapis.nocturne.mapping.model.Mapping;
import blue.lapis.nocturne.mapping.model.MemberMapping;
import blue.lapis.nocturne.mapping.model.MethodParameterMapping;
import blue.lapis.nocturne.processor.index.model.IndexedClass;
import blue.lapis.nocturne.processor.index.model.signature.FieldSignature;
import blue.lapis.nocturne.processor.index.model.signature.MemberSignature;
import blue.lapis.nocturne.processor.index.model.signature.MethodSignature;
import blue.lapis.nocturne.util.MemberType;
import blue.lapis.nocturne.util.helper.HierarchyHelper;
import blue.lapis.nocturne.util.helper.MappingsHelper;
import blue.lapis.nocturne.util.helper.StringHelper;
import com.google.common.collect.Sets;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Alert;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextInputDialog;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Text;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
/**
* Represents a selectable member in code.
*/
public class SelectableMember extends Text {
public static final Map<MemberKey, List<SelectableMember>> MEMBERS = new HashMap<>();
private final CodeTab codeTab;
private final MemberType type;
private final MemberKey key;
private final StringProperty nameProperty = new SimpleStringProperty(this, "name");
private final StringProperty descriptorProperty = new SimpleStringProperty(this, "descriptor");
private final StringProperty parentClassProperty = new SimpleStringProperty(this, "parentClass");
private final MemberSignature sig;
private boolean deobfuscated;
private String fullName = null; // only used for classes
public SelectableMember(CodeTab codeTab, MemberType type, String name) {
this(codeTab, type, name, null, null);
}
public SelectableMember(CodeTab codeTab, MemberType type, String name, String descriptor, String parentClass) {
super(name);
this.codeTab = codeTab;
this.type = type;
this.nameProperty.set(name);
this.descriptorProperty.set(descriptor);
if (type == MemberType.FIELD) {
this.sig = new FieldSignature(name, Type.fromString(descriptor));
} else if (type == MemberType.METHOD) {
this.sig = new MethodSignature(name, MethodDescriptor.fromString(descriptor));
} else {
this.sig = null;
}
this.parentClassProperty.set(parentClass);
if (type == MemberType.CLASS) {
fullName = getName();
}
this.key = new MemberKey(type, getQualifiedName(),
type == MemberType.FIELD || type == MemberType.METHOD ? descriptor : null);
this.setOnMouseClicked(event1 -> {
if (event1.getButton() == MouseButton.PRIMARY) {
this.updateCodeTab();
}
});
MenuItem renameItem = new MenuItem(Main.getResourceBundle().getString("member.contextmenu.rename"));
renameItem.setOnAction(event -> {
String dispText = this.getText();
if (getType() == MemberType.CLASS && !isInnerClass()) {
dispText = fullName;
}
TextInputDialog textInputDialog = new TextInputDialog(dispText);
textInputDialog.setHeaderText(Main.getResourceBundle().getString("member.contextmenu.rename"));
Optional<String> result = textInputDialog.showAndWait();
if (result.isPresent() && !result.get().equals("") && !result.get().equals(getText())) {
if ((getType() == MemberType.CLASS && !isInnerClass() && !checkClassDupe(result.get()))
|| ((getType() != MemberType.CLASS || isInnerClass()) && !checkMemberDupe(result.get()))) {
return;
}
String res = result.get();
if (getType() == MemberType.CLASS) {
res = DOT_PATTERN.matcher(res).replaceAll(CLASS_PATH_SEPARATOR_CHAR + "");
}
if ((getType() == MemberType.CLASS && !StringHelper.isJavaClassIdentifier(res))
|| (getType() != MemberType.CLASS && !StringHelper.isJavaIdentifier(res))) {
showIllegalAlert();
return;
}
this.setMapping(res);
}
});
MenuItem resetItem = new MenuItem(Main.getResourceBundle().getString("member.contextmenu.reset"));
resetItem.setOnAction(event -> {
if (getText().equals(getName())) {
Optional<? extends Mapping> mapping = getMapping();
if (mapping.isPresent()) {
mapping.get().setAdHoc(false);
if (mapping.get() instanceof MemberMapping) {
ClassMapping parent = ((MemberMapping) mapping.get()).getParent();
if (mapping.get() instanceof FieldMapping) {
//noinspection ConstantConditions
parent.removeFieldMapping((FieldSignature) sig);
} else {
//noinspection ConstantConditions
parent.removeMethodMapping((MethodSignature) sig);
}
} else if (mapping.get() instanceof MethodParameterMapping) {
((MethodParameterMapping) mapping.get()).getParent()
.removeParamMapping(mapping.get().getObfuscatedName());
}
}
MEMBERS.get(key).forEach(sm -> sm.setDeobfuscated(false));
}
switch (getType()) {
case CLASS: {
Optional<? extends Mapping> mapping = getMapping();
if (mapping.isPresent()
&& !mapping.get().getObfuscatedName().equals(mapping.get().getDeobfuscatedName())) {
if ((!isInnerClass() && !checkClassDupe(mapping.get().getObfuscatedName()))
|| (isInnerClass() && !checkMemberDupe(mapping.get().getObfuscatedName()))) {
break;
}
mapping.get().setDeobfuscatedName(mapping.get().getObfuscatedName());
mapping.get().setAdHoc(false);
setDeobfuscated(false);
}
fullName = getName();
break;
}
case FIELD:
case METHOD: {
Optional<ClassMapping> parent
= MappingsHelper.getClassMapping(Main.getMappingContext(), getParentClass());
if (parent.isPresent()) {
Optional<? extends Mapping> mapping = getMapping();
if (mapping.isPresent()) {
if (!checkMemberDupe(mapping.get().getObfuscatedName())) {
return;
}
if (getType() == MemberType.FIELD) {
//noinspection ConstantConditions
parent.get().removeFieldMapping((FieldSignature) sig);
} else {
//noinspection ConstantConditions
parent.get().removeMethodMapping((MethodSignature) sig);
}
MEMBERS.get(key).forEach(sm -> {
sm.setDeobfuscated(false);
sm.updateText();
});
}
}
break;
}
default: {
throw new AssertionError();
}
}
this.updateCodeTab();
});
MenuItem toggleDeobf = new MenuItem(Main.getResourceBundle().getString("member.contextmenu.toggleDeobf"));
toggleDeobf.setOnAction(event -> {
// I know this is gross but it's a hell of a lot easier than fixing the problem the "proper" way
boolean shouldDeobf = !this.deobfuscated;
genMapping().setAdHoc(!this.deobfuscated); // set as ad hoc if we need to mark it as deobfuscated
MEMBERS.get(key).forEach(sm -> sm.setDeobfuscated(shouldDeobf));
});
MenuItem jumpToDefItem = new MenuItem(Main.getResourceBundle().getString("member.contextmenu.jumpToDef"));
jumpToDefItem.setOnAction(event -> {
String className = getClassName();
Optional<ClassMapping> cm = MappingsHelper.getClassMapping(Main.getMappingContext(), className);
MainController.INSTANCE.openTab(className, cm.isPresent() ? cm.get().getDeobfuscatedName() : className);
});
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().add(renameItem);
contextMenu.getItems().add(resetItem);
contextMenu.getItems().add(toggleDeobf);
contextMenu.getItems().add(jumpToDefItem);
this.setOnContextMenuRequested(event -> {
Optional<? extends Mapping> mapping = getMapping();
toggleDeobf.setDisable(mapping.isPresent()
&& !mapping.get().getObfuscatedName().equals(mapping.get().getDeobfuscatedName()));
contextMenu.show(SelectableMember.this, event.getScreenX(), event.getScreenY());
});
if (!MEMBERS.containsKey(key)) {
MEMBERS.put(key, new ArrayList<>());
}
MEMBERS.get(key).add(this);
updateText();
Optional<? extends Mapping> mapping = getMapping();
setDeobfuscated(!getName().equals(fullName) || (mapping.isPresent() && mapping.get().isAdHoc()));
}
private String getClassName() {
String className = getType() == MemberType.CLASS ? getName() : getParentClass();
if (className.contains(INNER_CLASS_SEPARATOR_CHAR + "")) {
className = className.substring(0, className.indexOf(INNER_CLASS_SEPARATOR_CHAR));
}
return className;
}
private boolean checkClassDupe(String newName) {
if (Main.getLoadedJar().getCurrentNames().containsValue(newName)) {
showDupeAlert(false);
return false;
} else {
return true;
}
}
private boolean checkMemberDupe(String newName) {
switch (getType()) {
case CLASS: {
assert getName().contains(INNER_CLASS_SEPARATOR_CHAR + "");
Optional<JarClassEntry> jce = Main.getLoadedJar().getClass(getName()
.substring(0, getName().lastIndexOf(INNER_CLASS_SEPARATOR_CHAR)));
if (jce.isPresent()) {
if (jce.get().getCurrentInnerClassNames().containsValue(newName)) {
showDupeAlert(false);
return false;
} else {
return true;
}
} else {
return true;
}
}
case FIELD: {
JarClassEntry jce = Main.getLoadedJar().getClass(getParentClass()).get();
FieldSignature newSig = new FieldSignature(newName,
((FieldSignature) sig).getType());
if (jce.getCurrentFields().containsValue(newSig)) {
showDupeAlert(false);
return false;
} else {
return true;
}
}
case METHOD: {
Set<JarClassEntry> hierarchy = HierarchyHelper.getClassesInHierarchy(getParentClass(),
(MethodSignature) sig)
.stream().filter(c -> Main.getLoadedJar().getClass(c).isPresent())
.map(c -> Main.getLoadedJar().getClass(c).get()).collect(Collectors.toSet());
for (JarClassEntry jce : hierarchy) {
MethodSignature newSig
= new MethodSignature(newName, ((MethodSignature) sig).getDescriptor());
if (jce.getCurrentMethods().containsValue(newSig)) {
showDupeAlert(!jce.getName().equals(getName()));
return false;
}
}
return true;
}
default: {
throw new AssertionError();
}
}
}
private void showDupeAlert(boolean hierarchical) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(Main.getResourceBundle().getString("rename.dupe.title"));
alert.setHeaderText(null);
alert.setContentText(
Main.getResourceBundle().getString("rename.dupe.content" + (hierarchical ? ".hierarchy" : ""))
);
alert.showAndWait();
}
private void showIllegalAlert() {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(Main.getResourceBundle().getString("rename.illegal.title"));
alert.setHeaderText(null);
alert.setContentText(Main.getResourceBundle().getString("rename.illegal.content"));
alert.showAndWait();
}
public void setMapping(String mapping) {
switch (type) {
case CLASS: {
if (fullName.contains(INNER_CLASS_SEPARATOR_CHAR + "")) {
mapping = fullName.substring(0, fullName.lastIndexOf(INNER_CLASS_SEPARATOR_CHAR) + 1) + mapping;
}
MappingsHelper.genClassMapping(Main.getMappingContext(), getName(), mapping, true);
fullName = mapping;
break;
}
case FIELD: {
MappingsHelper.genFieldMapping(Main.getMappingContext(), getParentClass(), (FieldSignature) sig,
mapping);
break;
}
case METHOD: {
IndexedClass clazz = IndexedClass.INDEXED_CLASSES.get(getParentClass());
Set<IndexedClass> classes = Sets.newHashSet(clazz.getHierarchy());
classes.add(clazz);
for (IndexedClass ic : classes) {
//noinspection SuspiciousMethodCalls: sig must be a MethodSignature object
if (ic.getMethods().containsKey(sig)) {
genMethodMapping(Main.getMappingContext(), ic.getName(), (MethodSignature) sig, mapping, false);
}
}
break;
}
default: {
throw new AssertionError();
}
}
}
public void updateCodeTab() {
CodeTab.SelectableMemberType sType = CodeTab.SelectableMemberType.fromMemberType(this.type);
this.codeTab.setMemberType(sType);
this.codeTab.setMemberIdentifier(this.getText());
if (sType.isInfoEnabled()) {
this.codeTab.setMemberInfo(this.getDescriptor());
}
}
public StringProperty getNameProperty() {
return nameProperty;
}
public StringProperty getDescriptorProperty() {
return descriptorProperty;
}
public StringProperty getParentClassProperty() {
return parentClassProperty;
}
public MemberType getType() {
return type;
}
public String getName() {
return getNameProperty().get();
}
public String getDescriptor() {
return getDescriptorProperty().get();
}
public String getParentClass() {
return getParentClassProperty().get();
}
private void updateText() {
String deobf;
switch (this.getType()) {
case CLASS:
deobf = ClassMapping.deobfuscate(Main.getMappingContext(), getName());
if (!isInnerClass()) {
fullName = deobf;
}
break;
case FIELD:
case METHOD:
deobf = getName();
Optional<ClassMapping> classMapping
= MappingsHelper.getClassMapping(Main.getMappingContext(), getParentClass());
if (classMapping.isPresent()) {
Map<? extends MemberSignature, ? extends Mapping> mappings = getType() == MemberType.FIELD
? classMapping.get().getFieldMappings()
: classMapping.get().getMethodMappings();
Mapping mapping = mappings.get(getType() == MemberType.METHOD
? new MethodSignature(getName(), MethodDescriptor.fromString(getDescriptor()))
: new FieldSignature(getName(), Type.fromString(getDescriptor())));
if (mapping != null) {
deobf = mapping.getDeobfuscatedName();
}
}
break;
default:
throw new AssertionError();
}
setAndProcessText(deobf);
}
public static SelectableMember fromMatcher(CodeTab codeTab, Matcher matcher) {
MemberType type = matcher.group().startsWith(CLASS_PREFIX)
? MemberType.CLASS
: MemberType.valueOf(matcher.group(1));
if (type == MemberType.CLASS) {
return new SelectableMember(codeTab, type, matcher.group(1));
} else {
String qualName = matcher.group(2);
String descriptor = matcher.group(3);
int offset = qualName.lastIndexOf(CLASS_PATH_SEPARATOR_CHAR);
String simpleName = qualName.substring(offset + 1);
String parentClass = qualName.substring(0, offset);
try {
return new SelectableMember(codeTab, type, simpleName, descriptor, parentClass);
} catch (IllegalArgumentException ex) {
return null;
}
}
}
public void setAndProcessText(String text) {
setText(getType() == MemberType.CLASS ? StringHelper.unqualify(text) : text);
}
public static final class MemberKey {
private final MemberType type;
private final String qualName;
private final String descriptor;
public MemberKey(MemberType type, String qualifiedName, String descriptor) {
this.type = type;
this.qualName = qualifiedName;
this.descriptor = descriptor;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof MemberKey)) {
return false;
}
MemberKey key = (MemberKey) obj;
return Objects.equals(type, key.type)
&& Objects.equals(qualName, key.qualName)
&& Objects.equals(descriptor, key.descriptor);
}
@Override
public int hashCode() {
return Objects.hash(type, qualName, descriptor);
}
}
public boolean isInnerClass() {
return getType() == MemberType.CLASS && getName().contains(INNER_CLASS_SEPARATOR_CHAR + "");
}
public void setDeobfuscated(boolean deobfuscated) {
this.deobfuscated = deobfuscated;
getStyleClass().clear();
if (deobfuscated) {
getStyleClass().add("deobfuscated");
} else {
getStyleClass().add("obfuscated");
}
}
private Mapping genMapping() {
switch (getType()) {
case CLASS: {
return getOrCreateClassMapping(Main.getMappingContext(), getClassName());
}
case FIELD: {
return MappingsHelper.genFieldMapping(Main.getMappingContext(), getClassName(), (FieldSignature) sig,
getName());
}
case METHOD: {
return MappingsHelper.genMethodMapping(Main.getMappingContext(), getClassName(), (MethodSignature) sig,
getName(), false);
}
default: {
throw new AssertionError();
}
}
}
@SuppressWarnings("SuspiciousMethodCalls")
private Optional<? extends Mapping> getMapping() {
Optional<ClassMapping> classMapping = MappingsHelper.getClassMapping(Main.getMappingContext(), getClassName());
if (!classMapping.isPresent()) {
return classMapping;
}
switch (getType()) {
case CLASS: {
return classMapping;
}
case FIELD: {
return Optional.ofNullable(classMapping.get().getFieldMappings().get(sig));
}
case METHOD: {
return Optional.ofNullable(classMapping.get().getMethodMappings().get(sig));
}
default: {
throw new AssertionError();
}
}
}
private String getQualifiedName() {
String qualName;
IndexedClass ic = IndexedClass.INDEXED_CLASSES.get(getParentClass());
switch (type) {
case CLASS:
qualName = getName();
break;
case FIELD:
//noinspection SuspiciousMethodCalls: sig must be a FieldSignature object
if (!ic.getFields().containsKey(sig)) {
throw new IllegalArgumentException();
}
qualName = getParentClass() + CLASS_PATH_SEPARATOR_CHAR + getName();
break;
case METHOD:
String parent = null;
//noinspection SuspiciousMethodCalls: sig must be a MethodSignature object
if (ic.getMethods().containsKey(sig)) {
parent = getParentClass();
} else {
for (IndexedClass hc : ic.getHierarchy()) {
//noinspection SuspiciousMethodCalls: sig must be a MethodSignature object
if (hc.getMethods().containsKey(sig)) {
parent = hc.getName();
break;
}
}
}
if (parent == null) {
throw new IllegalArgumentException(); //TODO
}
qualName = parent + CLASS_PATH_SEPARATOR_CHAR + getName();
break;
default:
throw new AssertionError();
}
return qualName;
}
}