mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-11 11:45:27 +02:00
manual: updated beacon settings descriptin. Source: implemented plausibility checks for beacon texts
This commit is contained in:
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
||||
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
@@ -12,6 +13,10 @@ import kst4contest.model.ThreadStateMessage;
|
||||
* interval. Their enable flags and message templates remain independent. Every
|
||||
* run reads the current preferences, resolves global message variables and
|
||||
* sends only the categories which are currently enabled.</p>
|
||||
*
|
||||
* <p>Beacon messages use the regular outbound chat-message pipeline. They are
|
||||
* not assembled as raw ON4KST frames, because that would bypass the common
|
||||
* category, delimiter and message-text validation.</p>
|
||||
*/
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
@@ -39,35 +44,40 @@ public class BeaconTask extends TimerTask {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
reportStatus(THREAD_NICKNAME, true, "initialized", false);
|
||||
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(chatController.getChatPreferences());
|
||||
|
||||
sendMainCategoryBeacon(variableResolver);
|
||||
sendSecondCategoryBeacon(variableResolver);
|
||||
sendMainCategoryBeacon();
|
||||
sendSecondCategoryBeacon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the main-category beacon if it is currently enabled.
|
||||
*/
|
||||
private void sendMainCategoryBeacon(MessageVariableResolver variableResolver) {
|
||||
if (!chatController.getChatPreferences().isBcn_beaconsEnabledMainCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "off", false);
|
||||
private void sendMainCategoryBeacon() {
|
||||
if (!chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledMainCat()) {
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
false,
|
||||
"off",
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
.getLoginChatCategoryMain(),
|
||||
chatController.getChatPreferences()
|
||||
.getBcn_beaconTextMainCat(),
|
||||
"main category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
false,
|
||||
"invalid text",
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,37 +86,50 @@ public class BeaconTask extends TimerTask {
|
||||
+ " [BeaconTask, Info]: Sending main-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 1", true, "on", false);
|
||||
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
true,
|
||||
"on",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the second-category beacon if the second login and its beacon are
|
||||
* currently enabled.
|
||||
*/
|
||||
private void sendSecondCategoryBeacon(
|
||||
MessageVariableResolver variableResolver
|
||||
) {
|
||||
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()
|
||||
private void sendSecondCategoryBeacon() {
|
||||
if (!chatController.getChatPreferences()
|
||||
.isLoginToSecondChatEnabled()
|
||||
|| !chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledSecondCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
false,
|
||||
"off",
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategorySecond()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
.getLoginChatCategorySecond(),
|
||||
chatController.getChatPreferences()
|
||||
.getBcn_beaconTextSecondCat(),
|
||||
"second category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
false,
|
||||
"invalid text",
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,47 +138,65 @@ public class BeaconTask extends TimerTask {
|
||||
+ " [BeaconTask, Info]: Sending second-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
|
||||
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
true,
|
||||
"on",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the server-directed message after validating the resolved payload.
|
||||
* Resolves and validates one beacon before placing it in the regular outbound
|
||||
* message queue.
|
||||
*
|
||||
* <p>The resolved text is checked rather than only the configured template
|
||||
* because inserted values can increase the final message length.</p>
|
||||
* <p>The returned message contains only the public-chat payload and its chat
|
||||
* category. {@link WriteThread} creates the final ON4KST frame through
|
||||
* {@link On4KstProtocol#chatMessage(int, String)}. This prevents a configurable
|
||||
* beacon text from bypassing the common protocol validation.</p>
|
||||
*
|
||||
* @param categoryNumber ON4KST category number
|
||||
* @param resolvedText fully resolved beacon payload
|
||||
* @param category target ON4KST chat category
|
||||
* @param configuredText configured beacon template
|
||||
* @param categoryDescription text used in diagnostic output
|
||||
* @return prepared message, or {@code null} if the payload is invalid
|
||||
* @return prepared message, or {@code null} if the category or text is invalid
|
||||
*/
|
||||
private ChatMessage buildBeaconMessage(
|
||||
int categoryNumber,
|
||||
String resolvedText,
|
||||
ChatCategory category,
|
||||
String configuredText,
|
||||
String categoryDescription
|
||||
) {
|
||||
if (resolvedText == null
|
||||
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
|
||||
int actualLength = resolvedText == null ? 0 : resolvedText.length();
|
||||
try {
|
||||
if (category == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No chat category is configured."
|
||||
);
|
||||
}
|
||||
|
||||
On4KstProtocol.category(category.getCategoryNumber());
|
||||
|
||||
String resolvedText =
|
||||
chatController.resolveAndValidateBeaconText(
|
||||
configuredText
|
||||
);
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(resolvedText);
|
||||
beaconMessage.setChatCategory(category);
|
||||
beaconMessage.setMessageDirectedToServer(false);
|
||||
|
||||
return beaconMessage;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
System.out.println(
|
||||
"[BeaconTask, Warning]: Beacon for "
|
||||
+ categoryDescription
|
||||
+ " was not sent because the resolved text contains "
|
||||
+ actualLength
|
||||
+ " characters; maximum is "
|
||||
+ ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ "."
|
||||
+ " was not queued: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(
|
||||
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
|
||||
);
|
||||
beaconMessage.setMessageDirectedToServer(true);
|
||||
return beaconMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,6 +214,9 @@ public class BeaconTask extends TimerTask {
|
||||
information,
|
||||
criticalState
|
||||
);
|
||||
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
|
||||
callbackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
stateMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2827,6 +2827,82 @@ private ObservableList<String>
|
||||
this.dbHandler = dbHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates a configured beacon template before it is stored.
|
||||
*
|
||||
* <p>The template itself must be a non-empty, protocol-safe message. Global
|
||||
* variables are resolved as far as their values are currently available.
|
||||
* A template which temporarily resolves to an empty string, for example
|
||||
* {@code MYQRG} before a frequency is known, remains valid. The timer performs
|
||||
* the stricter final validation immediately before transmission.</p>
|
||||
*
|
||||
* @param configuredText configured beacon template
|
||||
* @throws IllegalArgumentException if the template contains invalid protocol
|
||||
* content or currently resolves to more than
|
||||
* {@value MAX_BEACON_TEXT_LENGTH} characters
|
||||
*/
|
||||
public void validateBeaconTemplate(String configuredText) {
|
||||
String normalizedTemplate =
|
||||
On4KstProtocol.messageText(configuredText);
|
||||
|
||||
String resolvedText =
|
||||
new MessageVariableResolver(chatPreferences)
|
||||
.resolveGlobalVariables(normalizedTemplate);
|
||||
|
||||
/*
|
||||
* A template consisting only of a variable such as MYQRG may temporarily
|
||||
* resolve to an empty value. It can still be stored because the value may
|
||||
* become available through TRX synchronisation before the timer runs.
|
||||
*/
|
||||
if (resolvedText == null || resolvedText.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateResolvedBeaconText(resolvedText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and validates the final beacon text immediately before transmission.
|
||||
*
|
||||
* <p>This is deliberately stricter than {@link #validateBeaconTemplate(String)}.
|
||||
* A timer run must never queue an empty message merely because a dynamic value
|
||||
* is not available at that moment.</p>
|
||||
*
|
||||
* @param configuredText configured beacon template
|
||||
* @return normalized text which may safely pass through the regular ON4KST
|
||||
* message pipeline
|
||||
* @throws IllegalArgumentException if the resolved message is empty, too long
|
||||
* or contains an ON4KST delimiter
|
||||
*/
|
||||
public String resolveAndValidateBeaconText(String configuredText) {
|
||||
String resolvedText =
|
||||
new MessageVariableResolver(chatPreferences)
|
||||
.resolveGlobalVariables(configuredText);
|
||||
|
||||
return validateResolvedBeaconText(resolvedText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the message-text and length rules to a fully resolved beacon.
|
||||
*/
|
||||
private String validateResolvedBeaconText(String resolvedText) {
|
||||
String normalizedText =
|
||||
On4KstProtocol.messageText(resolvedText);
|
||||
|
||||
if (normalizedText.length() > MAX_BEACON_TEXT_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"The resolved beacon message contains "
|
||||
+ normalizedText.length()
|
||||
+ " characters; maximum is "
|
||||
+ MAX_BEACON_TEXT_LENGTH
|
||||
+ "."
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the shared beacon timer with the interval currently stored in the
|
||||
* preferences.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@@ -7,6 +10,8 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
|
||||
class On4KstProtocolTest {
|
||||
@Test
|
||||
@@ -66,4 +71,72 @@ class On4KstProtocolTest {
|
||||
On4KstConnectionManager.parseMessageTimestamp(
|
||||
"CH|2|20260813123456|DL1ABC|Op|0|msg|0|"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesBeaconVariablesBeforeApplyingProtocolValidation() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
preferences.setMYQRGFirstCat("144.300");
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
controller.validateBeaconTemplate(
|
||||
"calling cq at MYQRG"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"calling cq at 144.300",
|
||||
controller.resolveAndValidateBeaconText(
|
||||
"calling cq at MYQRG"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsTemporarilyUnresolvedVariableOnlyBeaconTemplate() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
preferences.setMYQRGFirstCat("");
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
assertDoesNotThrow(
|
||||
() -> controller.validateBeaconTemplate("MYQRG")
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.resolveAndValidateBeaconText("MYQRG")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEmptyOverlongAndProtocolBreakingBeaconText() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(" ")
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(
|
||||
"cq at 144.300|0|QUIT"
|
||||
)
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(
|
||||
"x".repeat(
|
||||
ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ 1
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4933,40 +4933,51 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
|
||||
/**
|
||||
* Validates and stores one beacon text.
|
||||
* Validates and stores one beacon template.
|
||||
*
|
||||
* <p>The final text is checked after global variables have been resolved.
|
||||
* Otherwise a template with at most 120 characters could still exceed the
|
||||
* server limit after values such as MYQRG or MYLOCATOR were inserted.</p>
|
||||
* <p>The template is checked against the same protocol and length rules which
|
||||
* are applied by the timer before transmission. A variable-only template may
|
||||
* temporarily resolve to an empty value and can still be stored; the timer will
|
||||
* not send it until a usable value is available.</p>
|
||||
*
|
||||
* @param textField field containing the configured beacon template
|
||||
* @param mainCategory {@code true} for the main category, {@code false} for
|
||||
* the optional second category
|
||||
*/
|
||||
private void applyBeaconTextSetting(TextField textField, boolean mainCategory) {
|
||||
String configuredText = textField.getText() == null ? "" : textField.getText();
|
||||
String resolvedText = messageVariableResolver.resolveGlobalVariables(configuredText);
|
||||
private void applyBeaconTextSetting(
|
||||
TextField textField,
|
||||
boolean mainCategory
|
||||
) {
|
||||
String configuredText =
|
||||
textField.getText() == null
|
||||
? ""
|
||||
: textField.getText();
|
||||
|
||||
try {
|
||||
chatcontroller.validateBeaconTemplate(configuredText);
|
||||
|
||||
if (resolvedText != null
|
||||
&& resolvedText.length() <= ChatController.MAX_BEACON_TEXT_LENGTH) {
|
||||
if (mainCategory) {
|
||||
chatcontroller.getChatPreferences().setBcn_beaconTextMainCat(configuredText);
|
||||
chatcontroller.getChatPreferences()
|
||||
.setBcn_beaconTextMainCat(configuredText);
|
||||
} else {
|
||||
chatcontroller.getChatPreferences().setBcn_beaconTextSecondCat(configuredText);
|
||||
chatcontroller.getChatPreferences()
|
||||
.setBcn_beaconTextSecondCat(configuredText);
|
||||
}
|
||||
return;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
String previousText =
|
||||
mainCategory
|
||||
? chatcontroller.getChatPreferences()
|
||||
.getBcn_beaconTextMainCat()
|
||||
: chatcontroller.getChatPreferences()
|
||||
.getBcn_beaconTextSecondCat();
|
||||
|
||||
textField.setText(previousText);
|
||||
|
||||
alertWindowEvent(
|
||||
"The beacon message is invalid: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
String previousText = mainCategory
|
||||
? chatcontroller.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
: chatcontroller.getChatPreferences().getBcn_beaconTextSecondCat();
|
||||
|
||||
textField.setText(previousText);
|
||||
alertWindowEvent(
|
||||
"The resolved beacon message must not exceed "
|
||||
+ ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ " characters."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user