Skip to content

Commit 989438b

Browse files
committed
Enhance documentation and error handling across multiple classes; add configuration file for plugin settings
1 parent 5322df1 commit 989438b

15 files changed

Lines changed: 243 additions & 54 deletions

src/main/java/com/pyjavabridge/BridgeInstance.java

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@
8888
import org.bukkit.event.EventHandler;
8989
import org.bukkit.event.player.PlayerQuitEvent;
9090

91+
/**
92+
* Represents a single Python script bridge connection.
93+
* Manages the IPC protocol (stdin/stdout), message routing,
94+
* event subscriptions, and object handle registry for one script.
95+
*/
9196
public class BridgeInstance {
9297
private final PyJavaBridgePlugin plugin;
9398
private final String name;
@@ -152,7 +157,9 @@ public void onPlayerQuit(PlayerQuitEvent event) {
152157
if (attachment != null) {
153158
try {
154159
event.getPlayer().removeAttachment(attachment);
155-
} catch (Exception ignored) {
160+
} catch (Exception e) {
161+
// Attachment may already be invalid if plugin is disabling
162+
plugin.getLogger().fine("[" + name + "] Could not remove permission attachment: " + e.getMessage());
156163
}
157164
}
158165
}
@@ -179,6 +186,7 @@ public boolean hasSubscription(String eventName) {
179186
return subscriptions.containsKey(eventName);
180187
}
181188

189+
/** Starts the Python process and bridge communication thread. */
182190
void start() {
183191
running = true;
184192

@@ -214,7 +222,9 @@ private void shutdownInternal() {
214222
if (obj instanceof org.bukkit.boss.BossBar bar) {
215223
try {
216224
bar.removeAll();
217-
} catch (Exception ignored) {}
225+
} catch (Exception e) {
226+
plugin.getLogger().fine("[" + name + "] Could not remove boss bar: " + e.getMessage());
227+
}
218228
}
219229
}
220230

@@ -254,7 +264,7 @@ private void bridgeLoop() {
254264
} catch (IOException eof) {
255265
break;
256266
}
257-
if (length <= 0 || length > 16_777_216) {
267+
if (length <= 0 || length > plugin.getMaxMessageSize()) {
258268
plugin.getLogger().severe("[" + name + "] Invalid message length: " + length);
259269
break;
260270
}
@@ -267,6 +277,10 @@ private void bridgeLoop() {
267277
plugin.getLogger().severe("[" + name + "] Failed to parse message: " + e.getMessage());
268278
continue;
269279
}
280+
if (!message.has("type") || !message.get("type").isJsonPrimitive()) {
281+
plugin.getLogger().warning("[" + name + "] Received message without valid 'type' field, skipping");
282+
continue;
283+
}
270284
handleMessage(message);
271285
}
272286

@@ -406,6 +420,21 @@ private void handleHandshake(JsonObject message) {
406420

407421
private void handleRegisterCommand(JsonObject message) {
408422
String commandName = message.get("name").getAsString();
423+
424+
// Validate command name
425+
if (commandName == null || commandName.isEmpty()) {
426+
plugin.getLogger().warning("[" + name + "] Attempted to register command with empty name");
427+
return;
428+
}
429+
if (commandName.length() > 64) {
430+
plugin.getLogger().warning("[" + name + "] Command name too long (max 64): " + commandName);
431+
return;
432+
}
433+
if (!commandName.matches("[a-zA-Z0-9_-]+")) {
434+
plugin.getLogger().warning("[" + name + "] Invalid command name (alphanumeric, _ and - only): " + commandName);
435+
return;
436+
}
437+
409438
String permission = message.has("permission") ? message.get("permission").getAsString() : null;
410439
boolean hasDynamicTabComplete = message.has("has_tab_complete") && message.get("has_tab_complete").getAsBoolean();
411440

@@ -508,7 +537,8 @@ private void handleRemoveEntities(JsonObject message) {
508537
if (obj instanceof Entity entity) {
509538
try {
510539
entity.remove();
511-
} catch (Exception ignored) {
540+
} catch (Exception e) {
541+
plugin.getLogger().fine("[" + name + "] Could not remove entity: " + e.getMessage());
512542
}
513543
}
514544
registry.release(handle);
@@ -644,22 +674,32 @@ void sendShutdownEvent() {
644674

645675
public Object handleKick(Player player, List<Object> args) {
646676
String reason = args.isEmpty() ? "" : String.valueOf(args.get(0));
677+
// Try API variants in order: Component, String, legacy kickPlayer
647678
try {
648679
Method kick = player.getClass().getMethod("kick", Component.class);
649680
kick.invoke(player, Component.text(reason));
650681
return null;
651-
} catch (Exception ignored) {
682+
} catch (NoSuchMethodException e) {
683+
// API variant not available, try next
684+
} catch (Exception e) {
685+
plugin.getLogger().fine("[" + name + "] kick(Component) failed: " + e.getMessage());
652686
}
653687
try {
654688
Method kick = player.getClass().getMethod("kick", String.class);
655689
kick.invoke(player, reason);
656690
return null;
657-
} catch (Exception ignored) {
691+
} catch (NoSuchMethodException e) {
692+
// API variant not available, try next
693+
} catch (Exception e) {
694+
plugin.getLogger().fine("[" + name + "] kick(String) failed: " + e.getMessage());
658695
}
659696
try {
660697
Method kickPlayer = player.getClass().getMethod("kickPlayer", String.class);
661698
kickPlayer.invoke(player, reason);
662-
} catch (Exception ignored) {
699+
} catch (NoSuchMethodException e) {
700+
plugin.getLogger().warning("[" + name + "] No kick method found on player class");
701+
} catch (Exception e) {
702+
plugin.getLogger().fine("[" + name + "] kickPlayer(String) failed: " + e.getMessage());
663703
}
664704
return null;
665705
}
@@ -1613,7 +1653,8 @@ private Object invokeInventoryMethod(org.bukkit.inventory.Inventory inventory, S
16131653
viewer.closeInventory();
16141654
}
16151655
}
1616-
} catch (Exception ignored) {
1656+
} catch (Exception e) {
1657+
plugin.getLogger().fine("[" + name + "] Error closing inventory: " + e.getMessage());
16171658
}
16181659
return null;
16191660
}
@@ -1622,7 +1663,8 @@ private Object invokeInventoryMethod(org.bukkit.inventory.Inventory inventory, S
16221663
Method getTitle = inventory.getClass().getMethod("getTitle");
16231664
Object titleObj = getTitle.invoke(inventory);
16241665
return titleObj != null ? titleObj.toString() : "";
1625-
} catch (Exception ignored) {
1666+
} catch (Exception e) {
1667+
// getTitle() may not exist on all inventory types
16261668
return "";
16271669
}
16281670
}
@@ -2114,7 +2156,9 @@ private Object invokeItemStackMethod(ItemStack itemStack, String method, List<Ob
21142156
try {
21152157
Method setType = ItemStack.class.getMethod("setType", Material.class);
21162158
setType.invoke(itemStack, deserialized.getType());
2117-
} catch (Exception ignored) {
2159+
} catch (Exception e) {
2160+
// setType may be deprecated/removed in newer API versions
2161+
plugin.getLogger().fine("[" + name + "] ItemStack.setType fallback failed: " + e.getMessage());
21182162
}
21192163
itemStack.setAmount(deserialized.getAmount());
21202164
if (deserialized.hasItemMeta()) {
@@ -2501,7 +2545,7 @@ private Object invokeDisplayMethod(Object target, String method, List<Object> ar
25012545
return UNHANDLED;
25022546
}
25032547

2504-
// #2: Cache getMethods() per class to avoid repeated reflection
2548+
// Cache getMethods() per class to avoid repeated reflection
25052549
private static final ConcurrentHashMap<Class<?>, Method[]> reflectiveMethodsCache = new ConcurrentHashMap<>();
25062550

25072551
private Object invokeReflective(Object target, String method, List<Object> args) throws Exception {
@@ -2631,7 +2675,9 @@ private Sound resolveSound(Object arg) {
26312675
}
26322676
}
26332677
}
2634-
} catch (Exception ignored) {
2678+
} catch (Exception e) {
2679+
// Sound registry lookup may fail depending on server version
2680+
plugin.getLogger().fine("Sound registry lookup failed for '" + name + "': " + e.getMessage());
26352681
}
26362682
}
26372683

@@ -2644,7 +2690,9 @@ private Sound resolveSound(Object arg) {
26442690
if (soundObj instanceof Sound sound) {
26452691
return sound;
26462692
}
2647-
} catch (Exception ignored) {
2693+
} catch (Exception e) {
2694+
// Sound.valueOf not available or name doesn't match
2695+
plugin.getLogger().fine("Sound.valueOf failed for '" + enumName + "': " + e.getMessage());
26482696
}
26492697
}
26502698
return null;
@@ -2669,7 +2717,8 @@ private String getTabListValue(Player player, boolean header) {
26692717
Method method = player.getClass().getMethod(name);
26702718
value = method.invoke(player);
26712719
break;
2672-
} catch (Exception ignored) {
2720+
} catch (Exception e) {
2721+
// API variant not available; try next method name
26732722
}
26742723
}
26752724

@@ -2744,12 +2793,14 @@ private boolean tryTabListSetter(Player player, Object header, Object footer, Bo
27442793
method.invoke(player, header);
27452794
return true;
27462795

2747-
} catch (Exception ignored) {
2796+
} catch (Exception e) {
2797+
// Try next API method variant
27482798
}
27492799
}
27502800
return false;
27512801
}
27522802

2803+
/** Serializes a value using the bridge serializer. */
27532804
public JsonElement serialize(Object value) {
27542805
return serializer.serialize(value);
27552806
}
@@ -2915,6 +2966,7 @@ private JsonObject deserializePayload(byte[] payload) {
29152966
return JsonParser.parseString(new String(payload, StandardCharsets.UTF_8)).getAsJsonObject();
29162967
}
29172968

2969+
/** Sends a JSON message to the Python process over the bridge. */
29182970
public void send(JsonObject response) {
29192971
if (writer == null || !running) {
29202972
return;
@@ -2936,8 +2988,8 @@ public void send(JsonObject response) {
29362988
}
29372989

29382990
/**
2939-
* Write multiple responses under a single lock acquisition + single flush.
2940-
* Much more efficient than calling send() in a loop for batch responses.
2991+
* Write multiple responses under a single lock acquisition and single flush.
2992+
* More efficient than calling send() in a loop for batch responses.
29412993
*/
29422994
private void sendAll(List<JsonObject> responses, long startNano) {
29432995
if (writer == null || !running || responses.isEmpty()) {
@@ -3091,6 +3143,15 @@ private void startPythonProcess() {
30913143
}
30923144

30933145
private String resolvePythonExecutable() {
3146+
String override = plugin.getPythonExecutableOverride();
3147+
if (override != null && !"auto".equalsIgnoreCase(override)) {
3148+
Path overridePath = Path.of(override);
3149+
if (Files.exists(overridePath)) {
3150+
return overridePath.toAbsolutePath().toString();
3151+
}
3152+
plugin.getLogger().warning("[" + name + "] Configured python-executable not found: " + override + ", falling back to auto-detect");
3153+
}
3154+
30943155
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
30953156

30963157
Path venvDir = scriptsDir.resolve(".venv");
@@ -3113,7 +3174,8 @@ private void closeQuietly(AutoCloseable closable) {
31133174
}
31143175
try {
31153176
closable.close();
3116-
} catch (Exception ignored) {
3177+
} catch (Exception e) {
3178+
plugin.getLogger().fine("[" + name + "] Error during close: " + e.getMessage());
31173179
}
31183180
}
31193181
}

src/main/java/com/pyjavabridge/BridgeSerializer.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,25 @@
4444
import java.util.UUID;
4545
import java.util.concurrent.ConcurrentHashMap;
4646

47+
/**
48+
* Serializes and deserializes Java/Bukkit objects to and from JSON
49+
* for transmission over the Python bridge. Handles object registration,
50+
* cyclic reference detection, and type-specific field extraction.
51+
*/
4752
public class BridgeSerializer {
4853
private final ObjectRegistry registry;
4954
private final Gson gson;
5055
private final PyJavaBridgePlugin plugin;
5156

52-
// #9: Single cache map for tryInvokeNoArg — Optional.empty() means known-missing
57+
// Cache for tryInvokeNoArg — Optional.empty() means known-missing
5358
private final ConcurrentHashMap<String, Optional<Method>> noArgMethodCache = new ConcurrentHashMap<>();
5459
// Cache the PlainTextComponentSerializer instance instead of calling .plainText() repeatedly
5560
private static final PlainTextComponentSerializer PLAIN_TEXT = PlainTextComponentSerializer.plainText();
5661

57-
// #8: Cache getLogicalTypeName() per concrete class to avoid repeated instanceof chains
62+
// Cache getLogicalTypeName() per concrete class to avoid repeated instanceof chains
5863
private static final ConcurrentHashMap<Class<?>, String> typeNameCache = new ConcurrentHashMap<>();
5964

60-
// #3: ThreadLocal for cyclic reference detection set — reuse across serialize() calls
65+
// ThreadLocal for cyclic reference detection set — reuse across serialize() calls
6166
private static final ThreadLocal<Set<Object>> SEEN_SET = ThreadLocal.withInitial(
6267
() -> Collections.newSetFromMap(new java.util.IdentityHashMap<>()));
6368

@@ -100,6 +105,7 @@ public BridgeSerializer(ObjectRegistry registry, Gson gson, PyJavaBridgePlugin p
100105
this.plugin = plugin;
101106
}
102107

108+
/** Serializes a Java/Bukkit object to a JSON representation. */
103109
public JsonElement serialize(Object value) {
104110
Set<Object> seen = SEEN_SET.get();
105111
try {
@@ -316,7 +322,8 @@ private JsonElement serialize(Object value, Set<Object> seen) {
316322
if (titleObj != null) {
317323
fields.addProperty("title", titleObj.toString());
318324
}
319-
} catch (Exception ignored) {
325+
} catch (Exception e) {
326+
// getTitle() may not exist on all inventory implementations
320327
}
321328
}
322329

@@ -446,11 +453,13 @@ private Object tryInvokeNoArg(Object target, String methodName) {
446453
if (cached.isEmpty()) return null;
447454
try {
448455
return cached.get().invoke(target);
449-
} catch (Exception ignored) {
456+
} catch (Exception e) {
457+
// Invocation failed on a known method — likely transient entity state
450458
return null;
451459
}
452460
}
453461

462+
/** Deserializes a JSON element back to a Java object (primitives, lists, handles, refs, values). */
454463
public Object deserialize(JsonElement element) {
455464
if (element == null || element.isJsonNull()) {
456465
return null;

0 commit comments

Comments
 (0)