Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generate missing methods in SerializedOfflinePlayer at runtime #2777

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ private void writeObject(ObjectOutputStream output) throws IOException {

// Write the name of the player (or NULL if it's not set)
Player player = getPlayer();
output.writeObject(player != null ? new SerializedOfflinePlayer(player) : null);
output.writeObject(player != null ? SerializedOfflinePlayer.init(player) : null);
}

private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

Expand All @@ -61,7 +65,7 @@
*
* @author Kristian
*/
class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
abstract class SerializedOfflinePlayer implements OfflinePlayer, Serializable {

/**
* Generated by Eclipse.
Expand All @@ -84,11 +88,33 @@ class SerializedOfflinePlayer implements OfflinePlayer, Serializable {
private long lastSeen;

private static final Constructor<?> proxyPlayerConstructor = setupProxyPlayerConstructor();
private static final Constructor<? extends SerializedOfflinePlayer> CLASS_CONSTRUCTOR = setupClassConstructor();

/**
* Initialize a serializable offline player object from another offline player.
* <p>
* All other methods cause an exception.
*
* @param player - another offline player.
* @return A serializable offline player object.
*/
public static SerializedOfflinePlayer init(OfflinePlayer player) {
try {
CLASS_CONSTRUCTOR.setAccessible(true);
return CLASS_CONSTRUCTOR.newInstance(player);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access reflection.", e);
} catch (InstantiationException e) {
throw new RuntimeException("Cannot instantiate object.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error in invocation.", e);
}
}

/**
* Constructor used by serialization.
*/
public SerializedOfflinePlayer() {
protected SerializedOfflinePlayer() {
// Do nothing
}

Expand All @@ -97,7 +123,7 @@ public SerializedOfflinePlayer() {
*
* @param offline - another player.
*/
public SerializedOfflinePlayer(OfflinePlayer offline) {
protected SerializedOfflinePlayer(OfflinePlayer offline) {
this.name = offline.getName();
this.uuid = offline.getUniqueId();
this.firstPlayed = offline.getFirstPlayed();
Expand Down Expand Up @@ -342,6 +368,47 @@ public Player getProxyPlayer() {
}
}

private static Constructor<? extends SerializedOfflinePlayer> setupClassConstructor() {
final Method[] existingMethods = SerializedOfflinePlayer.class.getDeclaredMethods();
final Set<String> existingMethodNames = new HashSet<>();

for (int idx = 0; idx < existingMethods.length; idx++) {
existingMethodNames.add(existingMethods[idx].getName());
}

final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods();
final List<String> methodNamesToAdd = new ArrayList<>();

for (int idx = 0; idx < offlinePlayerMethods.length; idx++) {
final String name = offlinePlayerMethods[idx].getName();

if (!existingMethodNames.contains(name)) {
methodNamesToAdd.add(name);
}
}

final ElementMatcher.Junction<ByteCodeElement> missingMethods =
ElementMatchers.namedOneOf(methodNamesToAdd.toArray(new String[methodNamesToAdd.size()]));

final InvocationHandlerAdapter throwException = InvocationHandlerAdapter.of((obj, method, args) -> {
throw new UnsupportedOperationException(
"The method " + method.getName() + " is not supported.");
});

try {
return ByteBuddyFactory.getInstance()
.createSubclass(SerializedOfflinePlayer.class)
.method(missingMethods)
.intercept(throwException)
.make()
.load(ByteBuddyFactory.getInstance().getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded()
.getConstructor(OfflinePlayer.class);
} catch (NoSuchMethodException ex) {
throw new RuntimeException("Failed to find SerializedOfflinePlayer constructor!", ex);
}
}

private static Constructor<? extends Player> setupProxyPlayerConstructor() {
final Method[] offlinePlayerMethods = OfflinePlayer.class.getMethods();
final String[] methodNames = new String[offlinePlayerMethods.length];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void initMocks() {
when(offlinePlayer.hasPlayedBefore()).thenReturn(playedBefore);
when(offlinePlayer.isWhitelisted()).thenReturn(whitelisted);

serializedOfflinePlayer = new SerializedOfflinePlayer(offlinePlayer);
serializedOfflinePlayer = SerializedOfflinePlayer.init(offlinePlayer);
}

@Test
Expand Down
Loading