Class ServerPropertiesManager

java.lang.Object
com.ultikits.ultitools.manager.ServerPropertiesManager

@Internal public class ServerPropertiesManager extends Object
Safe server.properties manager for remote editing via WebSocket. Only exposes a curated whitelist of non-sensitive keys.

Why a panel edit cannot overwrite operator content (maintainer decisions of 2026-10-04, "operator-written configuration is never overwritten" and "what code may write, by file type": a panel edit writes only the named key; UltiKits/UltiTools-Reborn#607). The file is never re-emitted: an edit replaces the value text of the one physical line that defines the key and nothing else - the header and operator comments, key order, the key text and separator as written, line terminators and every other value keep their bytes. The file is decoded the way the running server reads it (measured from the Paper 1.21.11 jar, Settings#loadFromFile: strict UTF-8, falling back to ISO-8859-1 when the file is not valid UTF-8), the new value is escaped with the Properties value grammar in a form that reader returns exactly, and the result is re-encoded in the charset it was decoded with. Before publishing, every other line is checked byte for byte and a fresh parse must return the new value and every other key unchanged; a key defined on more than one line, or over continuation lines, is refused with a reason. Publication is atomic through AtomicConfigWriter, and only while the file still holds the bytes the edit was made against. A refusal writes nothing and logs one WARNING naming the file, the key and the reason, never a value.

  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • WRITE_LOCK

      private static final Object WRITE_LOCK
      Serializes panel edits of the file within this server run.
    • FILE_NAME

      private static final String FILE_NAME
      See Also:
    • serverRoot

      private final File serverRoot
    • webSocketClient

      private UltiPanelWebSocketClient webSocketClient
    • SAFE_KEYS

      private static final Set<String> SAFE_KEYS
  • Constructor Details

    • ServerPropertiesManager

      public ServerPropertiesManager(File serverRoot)
  • Method Details

    • setWebSocketClient

      public void setWebSocketClient(UltiPanelWebSocketClient client)
    • getSafeProperties

      public Map<String,String> getSafeProperties()
      The whitelisted keys the file defines, decoded the way the server reads them (see the class description).
      Returns:
      key to value; empty when the file is absent, unreadable or unparseable
    • setProperty

      public boolean setProperty(String key, String value)
    • writeProperty

      private ServerPropertiesManager.WriteResult writeProperty(String key, String value)
      Why this exists next to setProperty(String, String): the boolean is lossy. A false could mean "the key is not on the whitelist", "the running server server's server.properties does not have the key", "there is no server.properties to write to", or "the write itself failed" — four situations with four different fixes, collapsed into one value. The batch path has to tell the caller which one happened, so the real outcome is produced here and setProperty keeps its original contract by narrowing it.

      The write changes only the line defining key (see the class description for why nothing else can change); a refusal carries its reason.

    • refuse

      private static ServerPropertiesManager.WriteResult refuse(Path path, String key, String reason)
    • lineNumbers

      private static String lineNumbers(List<ServerPropertiesManager.Definition> definitions)
    • isWhitespace

      private static boolean isWhitespace(char c)
    • endsWithOddBackslashes

      private static boolean endsWithOddBackslashes(CharSequence line)
    • splitKey

      private static int[] splitKey(CharSequence line)
      Where the key ends and the value starts in one logical line, exactly as Properties#load0 decides: the key ends at the first unescaped =, : or whitespace; then whitespace, at most one = or :, and whitespace are skipped.
      Returns:
      [keyLength, valueStart]
    • unescape

      private static String unescape(String escaped)
      Properties#loadConvert: \uXXXX, \t \n \r \f, and \x for any other x.
    • escape

      private static String escape(String value, boolean utf8)
      Properties#saveConvert for a value: a leading space, \, = : # ! and the four control escapes are escaped; any other control character, a lone surrogate, and - in a file the server reads as ISO-8859-1 - every character above ~ is written as \uXXXX, which the reader decodes in both charsets. In a UTF-8 file other characters are written as they are, as the server itself writes them.
    • byteLines

      private static List<byte[]> byteLines(byte[] bytes)
      Splits bytes into lines, each with its terminator (LF, CR or CRLF).
    • describeOutcome

      private static String describeOutcome(ServerPropertiesManager.WriteOutcome outcome)
      One line naming why a single-key write did not happen, or null when it did. Exposed through handleSet(JsonObject)'s response so a panel operator is told the reason, not just success: false.
    • handleServerProperties

      public void handleServerProperties(com.google.gson.JsonObject data)
      Handle WebSocket message for server properties operations.
      Parameters:
      data - the message data
    • handleGet

      private void handleGet()
    • handleSet

      private void handleSet(com.google.gson.JsonObject data)
    • handleSetAll

      private void handleSetAll(com.google.gson.JsonObject data)
    • applySetAll

      public ServerPropertiesManager.SetAllResult applySetAll(com.google.gson.JsonObject values)
      Apply a batch of properties, report it over the socket, and hand the outcome back.

      The return value is the point. handleSetAll does not need it — it only ever builds a message — but update_config with fileName: server_properties routes through here too, and its config_update_response is supposed to distinguish "delivered" from "took effect". Before this returned anything, that path had no way to learn the answer and reported success unconditionally.

      Parameters:
      values - key → value, an explicit JSON null meaning "leave this key alone"
      Returns:
      what happened to each key
    • buildSetAllResponse

      private com.google.gson.JsonObject buildSetAllResponse(ServerPropertiesManager.SetAllResult result)
    • toJsonArray

      private static com.google.gson.JsonArray toJsonArray(List<String> keys)
    • warnIfIncomplete

      private void warnIfIncomplete(ServerPropertiesManager.SetAllResult result)
      The response above is what the panel reads; this is what a server operator reads. Both are needed: someone changing a setting that silently does not apply otherwise finds nothing on either side.
    • sendResponse

      private void sendResponse(com.google.gson.JsonObject response)