From 94fc13e3c3d044fe53183026ef499028caadfbdf Mon Sep 17 00:00:00 2001 From: Marc Froehlich Date: Fri, 28 Aug 2026 23:19:56 +0200 Subject: [PATCH] refactoring of the SimpleLogFile-parser --- .../controller/ChatController.java | 112 +++++- .../controller/StatusUpdateListener.java | 13 +- .../controller/UCXLogFileToHashsetParser.java | 90 ++--- .../controller/UserActualizationTask.java | 365 ++++-------------- .../view/Kst4ContestApplication.java | 25 ++ .../ChatControllerSimpleLogTest.java | 51 +++ .../controller/UserActualizationTaskTest.java | 76 ++++ .../test/TestUCXLogFileToHashsetParser.java | 46 ++- 8 files changed, 414 insertions(+), 364 deletions(-) create mode 100644 src/test/java/kst4contest/controller/ChatControllerSimpleLogTest.java create mode 100644 src/test/java/kst4contest/controller/UserActualizationTaskTest.java diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 4232773c..6d2f0647 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -3,6 +3,7 @@ package kst4contest.controller; import java.net.*; import java.nio.ByteBuffer; import java.nio.channels.Selector; +import java.nio.file.Path; import java.sql.SQLException; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -2016,6 +2017,115 @@ private ObservableList return matchingMembers; } + /** + * Applies the global Worked state from the Simplelogfile to every active + * callsign variant with the same base callsign. UI-backed state is changed only + * on the JavaFX Application Thread. + * + * @param workedBaseCalls normalized callsigns detected in the selected file + */ + public void applySimpleLogWorkedBaseCalls(Set workedBaseCalls) { + Set normalizedWorkedBaseCalls = normalizeWorkedBaseCalls(workedBaseCalls); + if (normalizedWorkedBaseCalls.isEmpty()) { + return; + } + + runOnFxThread(() -> { + int changedMembers = markSimpleLogWorkedMembers( + activeChatMembersByCallAndCategory.values(), normalizedWorkedBaseCalls); + int changedClusterMessages = markSimpleLogWorkedClusterMessages( + lst_clusterMemberList, normalizedWorkedBaseCalls); + + if (changedMembers > 0) { + fireUserListUpdate("Simplelogfile Worked status updated"); + } + + LOGGER.log(Level.FINE, + "Simplelogfile marked {0} active members and {1} cluster messages as worked.", + new Object[] { changedMembers, changedClusterMessages }); + }); + } + + static int markSimpleLogWorkedMembers( + Collection members, + Set workedBaseCalls + ) { + if (members == null || workedBaseCalls == null || workedBaseCalls.isEmpty()) { + return 0; + } + + int changed = 0; + for (ChatMember member : members) { + if (member == null || member.isWorked()) { + continue; + } + + String memberCall = member.getCallSignRaw() != null + ? member.getCallSignRaw() : member.getCallSign(); + String baseCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall); + if (baseCall != null && workedBaseCalls.contains(baseCall.toUpperCase(Locale.ROOT))) { + member.setWorked(true); + changed++; + } + } + return changed; + } + + static int markSimpleLogWorkedClusterMessages( + Collection clusterMessages, + Set workedBaseCalls + ) { + if (clusterMessages == null || workedBaseCalls == null || workedBaseCalls.isEmpty()) { + return 0; + } + + int changed = 0; + for (ClusterMessage clusterMessage : clusterMessages) { + if (clusterMessage == null || clusterMessage.isReceiverWkd() + || clusterMessage.getReceiver() == null) { + continue; + } + + ChatMember receiver = clusterMessage.getReceiver(); + String receiverCall = receiver.getCallSignRaw() != null + ? receiver.getCallSignRaw() : receiver.getCallSign(); + String baseCall = ChatMember.normalizeCallSignToBaseCallSign(receiverCall); + if (baseCall != null && workedBaseCalls.contains(baseCall.toUpperCase(Locale.ROOT))) { + clusterMessage.setReceiverWkd(true); + changed++; + } + } + return changed; + } + + private static Set normalizeWorkedBaseCalls(Set workedBaseCalls) { + if (workedBaseCalls == null || workedBaseCalls.isEmpty()) { + return Set.of(); + } + + Set normalizedCalls = new HashSet<>(); + for (String callSign : workedBaseCalls) { + String baseCall = ChatMember.normalizeCallSignToBaseCallSign(callSign); + if (baseCall != null && !baseCall.isBlank()) { + normalizedCalls.add(baseCall.toUpperCase(Locale.ROOT)); + } + } + return Set.copyOf(normalizedCalls); + } + + /** + * Notifies the UI after a missing Simplelogfile was created successfully. + * + * @param filePath absolute path of the new file + */ + public void notifySimpleLogFileCreated(Path filePath) { + if (filePath == null || statusListener == null) { + return; + } + + runOnFxThread(() -> statusListener.onSimpleLogFileCreated(filePath)); + } + /** * Copies the band-specific NOT-QRV state to every active category variant of the * same base callsign. The database already uses callSignRaw as its key; applying @@ -4098,4 +4208,4 @@ private ObservableList return "Sniffed: (" + senderCall + " > " + receiverCall + ") " + msgText; } -} \ No newline at end of file +} diff --git a/src/main/java/kst4contest/controller/StatusUpdateListener.java b/src/main/java/kst4contest/controller/StatusUpdateListener.java index 45ba4cf0..2c205c7b 100644 --- a/src/main/java/kst4contest/controller/StatusUpdateListener.java +++ b/src/main/java/kst4contest/controller/StatusUpdateListener.java @@ -1,5 +1,7 @@ package kst4contest.controller; +import java.nio.file.Path; + import kst4contest.model.ThreadStateMessage; public interface StatusUpdateListener { @@ -33,4 +35,13 @@ public interface StatusUpdateListener { // Optional for non-UI listeners. } -} \ No newline at end of file + /** + * Called after KST4Contest successfully creates a missing Simplelogfile. + * + * @param filePath absolute path of the newly created file + */ + default void onSimpleLogFileCreated(Path filePath) { + // Optional for non-UI listeners. + } + +} diff --git a/src/main/java/kst4contest/controller/UCXLogFileToHashsetParser.java b/src/main/java/kst4contest/controller/UCXLogFileToHashsetParser.java index a2a1ae73..7c9bbfc2 100644 --- a/src/main/java/kst4contest/controller/UCXLogFileToHashsetParser.java +++ b/src/main/java/kst4contest/controller/UCXLogFileToHashsetParser.java @@ -1,12 +1,13 @@ package kst4contest.controller; import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; -import java.util.HashMap; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -14,73 +15,50 @@ import kst4contest.model.ChatMember; public class UCXLogFileToHashsetParser { - public BufferedReader fileReader; -// private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work - private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})"; - + private static final Pattern CALL_SIGN_PATTERN = Pattern.compile( + "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)" + + "|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)" + + "|([A-Z]\\d{2}[A-Z]{1,3})"); + private final Path logFile; public UCXLogFileToHashsetParser(String filePathAndName) { - - try { - fileReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(filePathAndName)))); - - } catch (FileNotFoundException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + this.logFile = Path.of(filePathAndName); } - /** - * check if a line of the ucxlog-Logfile inhibits a Callsign
- * returns ChatMember = null, if no frequency found - * - * @param chatMessage - */ - private ChatMember checkIfLineInhibitsCallSign(String line) { - - Pattern pattern = Pattern.compile(PTRN_CallSign); - Matcher matcher = pattern.matcher(line); - - String matchedString = ""; + private String findLastCallSign(String line) { + Matcher matcher = CALL_SIGN_PATTERN.matcher(line); + String matchedCallSign = ""; while (matcher.find()) { - - matchedString = matcher.group(); -// System.out.println("[UCXLogFile:] Processed worked Callsign from file: " + matchedString); - + matchedCallSign = matcher.group(); } - ChatMember newChatMember = new ChatMember(); - - newChatMember.setCallSign(matchedString.toUpperCase()); - - return newChatMember; - + return matchedCallSign.toUpperCase(Locale.ROOT); } /** - * Parses an ucxlog-live-file (full qualified path given by constructor - * argument), looks by regex for callsigns and builds a hashmap with only one - * entry by callsign + * Parses the selected log file and returns every detected station as a + * normalized base callsign. The reader is closed after each pass so the logging + * application can continue replacing or rotating the file. + * + * @return unique normalized base callsigns found in the file + * @throws IOException if the file cannot be read */ - public HashMap parse() throws IOException { + public Set parse() throws IOException { + Set workedBaseCalls = new HashSet<>(); - HashMap chatMemberMap = new HashMap(); - - String line; - while ((line = fileReader.readLine()) != null) { -// System.out.println("raw: " + line); - ChatMember temp = checkIfLineInhibitsCallSign(line); - - if (temp.getCallSign() != "") { - chatMemberMap.put(temp.getCallSign(), temp.getCallSign()); + try (BufferedReader fileReader = Files.newBufferedReader(logFile, Charset.defaultCharset())) { + String line; + while ((line = fileReader.readLine()) != null) { + String matchedCallSign = findLastCallSign(line); + String baseCallSign = ChatMember.normalizeCallSignToBaseCallSign(matchedCallSign); + if (baseCallSign != null && !baseCallSign.isBlank()) { + workedBaseCalls.add(baseCallSign.toUpperCase(Locale.ROOT)); + } } } -// System.out.println(chatMemberMap.size()); - return chatMemberMap; + return workedBaseCalls; } - } diff --git a/src/main/java/kst4contest/controller/UserActualizationTask.java b/src/main/java/kst4contest/controller/UserActualizationTask.java index 565f8b97..5186367f 100644 --- a/src/main/java/kst4contest/controller/UserActualizationTask.java +++ b/src/main/java/kst4contest/controller/UserActualizationTask.java @@ -1,325 +1,100 @@ package kst4contest.controller; -import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.Iterator; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Set; import java.util.TimerTask; - -import javafx.collections.ObservableList; -import kst4contest.model.ChatMember; -import kst4contest.model.ClusterMessage; -import kst4contest.view.GuiUtils; +import java.util.logging.Level; +import java.util.logging.Logger; public class UserActualizationTask extends TimerTask { - private ChatController client; + private static final Logger LOGGER = Logger.getLogger(UserActualizationTask.class.getName()); + + private final ChatController client; public UserActualizationTask(ChatController client) { - this.client = client; - } @Override public void run() { - Thread.currentThread().setName("UserActualizationTask"); - /* - * File-based log synchronization is optional. Do not create, open or parse the - * configured file while the feature is disabled. - */ + try { + updateWorkedCallSignsFromFile(); + } catch (RuntimeException exception) { + LOGGER.log(Level.WARNING, + "Unexpected failure while updating Worked callsigns from the Simplelogfile; " + + "the periodic task will continue.", + exception); + } + } + + private void updateWorkedCallSignsFromFile() { if (!client.getChatPreferences().isLogsynch_fileBasedWkdCallInterpreterEnabled()) { return; } -// System.out.println("[Useract: ] Thread runned now"); - -// System.out.println("***********************Useract started"); - - /** - * ******************************************since here: old mechanic for - * marking worked stations by .ucx-file - */ - - HashMap fetchedWorkedSet = new HashMap<>(); -// HashMap fetchedWorkedSetUdpBckup = new HashMap<>(); - - File f = new File(this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly()); - if (!f.exists() && !f.isDirectory()) { - try { - f.createNewFile(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + String configuredFileName = client.getChatPreferences() + .getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly(); + if (configuredFileName == null || configuredFileName.isBlank()) { + LOGGER.warning("Cannot read the Simplelogfile because no file is selected."); + return; } - UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser( - this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly()); + final Path logFile; + try { + logFile = Path.of(configuredFileName).toAbsolutePath().normalize(); + } catch (InvalidPathException exception) { + LOGGER.log(Level.WARNING, + "Cannot use the configured Simplelogfile path: " + configuredFileName, + exception); + return; + } + + boolean created = createMissingLogFile(logFile); + if (!Files.isRegularFile(logFile)) { + return; + } try { - fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse(); - - System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size()); - - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Set workedBaseCalls = new UCXLogFileToHashsetParser(logFile.toString()).parse(); + client.applySimpleLogWorkedBaseCalls(workedBaseCalls); + LOGGER.log(Level.FINE, + "Read {0} unique base callsigns from Simplelogfile {1}.", + new Object[] { workedBaseCalls.size(), logFile }); + } catch (IOException exception) { + LOGGER.log(Level.WARNING, "Cannot read Simplelogfile " + logFile, exception); } - ObservableList praktiKSTActiveUserList = this.client.getLst_chatMemberList(); - - for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) { - ChatMember chatMember = (ChatMember) iterator.next(); - -// System.out.println(chatMember.getCallSign()); -// System.out.println("USERACT active user list entries " + praktiKSTActiveUserList.size()); - - if (fetchedWorkedSet.containsKey(chatMember.getCallSign())) { - chatMember.setWorked(true); - System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() - + " as worked, based on READONLY-Logfile."); - } - -// if (fetchedWorkedSetUdpBckup.containsKey(chatMember.getCallSign())) { -// chatMember.setWorked(true); -// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked, based on UDPLsnBackup-Logfile."); -// } -// GuiUtils.triggerGUIFilteredChatMemberListChange(this.client); //todo: quick and dirty gui fix + if (created) { + client.notifySimpleLogFileCreated(logFile); } - - ObservableList praktiKSTClusterList = this.client.getLst_clusterMemberList(); - - for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) { - ClusterMessage clusterMessage = (ClusterMessage) iterator.next(); - - if (fetchedWorkedSet.containsKey(clusterMessage.getReceiver().getCallSign())) { - clusterMessage.setReceiverWkd(true); - System.out.println("[USERACT, info:] marking Clusterspotted " - + clusterMessage.getReceiver().getCallSign() + " as worked."); - } - -// if (fetchedWorkedSetUdpBckup.containsKey(clusterMessage.getReceiver().getCallSign())) { -// clusterMessage.setReceiverWkd(true); -// System.out.println("[USERACT, info:] marking Clusterspotted " -// + clusterMessage.getReceiver().getCallSign() + " as worked."); -// } - - } - - /** - * ******************************************end here: old mechanic for marking - * worked stations by .ucx-file - * - */ - - /** - * ******************************************since here: new mechanic for - * marking worked stations by udp/adif based information - */ -// HashMap fetchedWorkedMap = new HashMap<>(); -// -// fetchedWorkedMap = this.client.getMap_ucxLogInfoWorkedCalls(); - -// ObservableList praktiKSTActiveUserList1 = this.client.getLst_chatMemberList(); -// -// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) { -// ChatMember chatMember = (ChatMember) iterator.next(); -// -// if (fetchedWorkedMap.containsKey(chatMember.getCallSign())) { -// chatMember.setWorked(true); -// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked based on UDP Log Info Collector."); -// } -// } - -// ObservableList praktiKSTClusterList1 = this.client.getLst_clusterMemberList(); -// -// for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) { -// ClusterMessage clusterMessage = (ClusterMessage) iterator.next(); -// -// if (fetchedWorkedMap.containsKey(clusterMessage.getReceiver().getCallSign())) { -// clusterMessage.setReceiverWkd(true); -// System.out.println("[USERACT, info:] marking Clusterspotted " -// + clusterMessage.getReceiver().getCallSign() + " as worked based on UDP Log Info Collector."); -// } -// -// } - -// -// UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser( -// "C:\\UcxLog\\Logs\\DO5AMF\\DVU322_I.UCX"); -// try { -// fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse(); -// } catch (IOException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } -// -// ObservableList praktiKSTActiveUserList = this.client.getLst_chatMemberList(); -// -// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) { -// ChatMember chatMember = (ChatMember) iterator.next(); -// -// if (fetchedWorkedSet.containsKey(chatMember.getCallSign())) { -// chatMember.setWorked(true); -// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked."); -// } -// } -// -// -// ObservableList praktiKSTClusterList = this.client.getLst_clusterMemberList(); -// -// for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) { -// ClusterMessage clusterMessage = (ClusterMessage) iterator.next(); -// -// if (fetchedWorkedSet.containsKey(clusterMessage.getReceiver().getCallSign())) { -// clusterMessage.setReceiverWkd(true); -// System.out.println("[USERACT, info:] marking Clusterspotted " -// + clusterMessage.getReceiver().getCallSign() + " as worked."); -// } -// -// } - - /** - * ******************************************end here: new mechanic for marking - * worked stations by udp/adif based information - * - */ - -// System.out.println("[UserActualizationtask:] Userlist actualization will be performed now. " -// + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), -// ZoneId.systemDefault())); - -// ChatMessage actualizeUserMsg = new ChatMessage(); -// actualizeUserMsg.setDirectedToServer(true); -// actualizeUserMsg.setMessage("/show users"); - -// client.getMessageTXBus().add(actualizeUserMsg); - -// Enumeration e = this.client.getChatMemberTable().keys(); -// -// while (e.hasMoreElements()) { -// String key = e.nextElement(); -// -// System.out.println(this.client.getChatMemberTable().get(key).getCallSign() + ", " -// + this.client.getChatMemberTable().get(key).getQra() + ": " -// + this.client.getChatMemberTable().get(key).getFrequency()); -// -// } - -// System.out.println("[UserAct]: Show the Cluster with known frequencies now: "); -// -// Enumeration e2 = this.client.getdXClusterMemberTable().keys(); -// -// while (e2.hasMoreElements()) { -// String key = e2.nextElement(); -// -// System.out.println(this.client.getdXClusterMemberTable().get(key).getCallSign() + ", " -// + this.client.getdXClusterMemberTable().get(key).getQra() + ": " -// + this.client.getdXClusterMemberTable().get(key).getFrequency()); -// -// } - -// for (int i = 0; i < 100; i++) { -// -// System.out.print("\n"); -// } - - /** - * keeepalive start - */ -// ChatMessage keepAliveMSG = new ChatMessage(); -// keepAliveMSG.setMessageText("\r"); -// keepAliveMSG.setMessageDirectedToServer(true); -// -// System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " [UserAct]: Sending keepalive: " -// + keepAliveMSG.getMessageText()); -// /** -// * Sending keepalive -// */ -// this.client.getMessageTXBus().add(keepAliveMSG); - - /** - * keeepalive end - */ - -// System.out.println("[UserAct]: Show the Userlist with known frequencies sorted now: "); - -// ObservableList userlist = this.client.getLst_chatMemberList(); - -// for (Iterator iterator = userlist.iterator(); iterator.hasNext();) { -// ChatMember chatMember = (ChatMember) iterator.next(); -// System.out.println("[Useract] Entry " + this.client.getLst_chatMemberList().indexOf(chatMember) + ": " + chatMember.getCallSign()); -// } - -// -// String chatMembers =""; - -// SortedSet keys = new TreeSet<>(this.client.getChatMemberTable().keySet()); -// for (String key : keys) { -// -// chatMembers += this.client.getChatMemberTable().get(key).getCallSign() + ", " -// + this.client.getChatMemberTable().get(key).getName() + " in " -// + this.client.getChatMemberTable().get(key).getQra() + " @ QRG: " -// + this.client.getChatMemberTable().get(key).getFrequency() + "\n"; -// -// System.out.println(this.client.getChatMemberTable().get(key).getCallSign() + ", " -// + this.client.getChatMemberTable().get(key).getName() + " in " -// + this.client.getChatMemberTable().get(key).getQra() + " @ QRG: " -// + this.client.getChatMemberTable().get(key).getFrequency()); -// } - -// System.out.println("\n[UserAct]: Show the Clusterlist with known frequencies sorted now: "); - -// String dxcMembers =""; - -// SortedSet keys2 = new TreeSet<>(this.client.getdXClusterMemberTable().keySet()); -// for (String key : keys2) { -// System.out.println(this.client.getdXClusterMemberTable().get(key).getCallSign() + " in " -// + this.client.getdXClusterMemberTable().get(key).getQra() + " @ QRG: " -// + this.client.getdXClusterMemberTable().get(key).getFrequency()); -// -// dxcMembers += this.client.getdXClusterMemberTable().get(key).getCallSign() + " in " -// + this.client.getdXClusterMemberTable().get(key).getQra() + " @ QRG: " -// + this.client.getdXClusterMemberTable().get(key).getFrequency(); -// -// } - -// File userListLogger = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_userlist.txt"); -// -// FileWriter fileWriterRAWChatMSGOut = null; -// -// try { -// fileWriterRAWChatMSGOut = new FileWriter(userListLogger, true); -// } catch (IOException e1) { -// // TODO Auto-generated catch block -// e1.printStackTrace(); -// } -// -// BufferedWriter bufwrtrRawMSGOut; -// -// bufwrtrRawMSGOut = new BufferedWriter(fileWriterRAWChatMSGOut); - -// System.out.println("#######################################" + chatMembers); -// try { -// bufwrtrRawMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " " +this.client.getChatMemberTable().size() + " Chatmembers:\n" + chatMembers+ "\n"); -// bufwrtrRawMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " " + this.client.getdXClusterMemberTable().size() + " Clusterentries:\n" + dxcMembers + "\n"); - -// bufwrtrRawMSGOut.flush(); - -// } catch (IOException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } - -// try { -// bufwrtrRawMSGOut.close(); -// } catch (IOException e) { - // TODO Auto-generated catch block -// e.printStackTrace(); -// } } + private boolean createMissingLogFile(Path logFile) { + if (Files.exists(logFile)) { + if (!Files.isRegularFile(logFile)) { + LOGGER.log(Level.WARNING, + "The selected Simplelogfile path is not a regular file: {0}", + logFile); + } + return false; + } + + try { + Files.createFile(logFile); + LOGGER.log(Level.INFO, "Created missing Simplelogfile {0}.", logFile); + return true; + } catch (FileAlreadyExistsException exception) { + return false; + } catch (IOException | SecurityException exception) { + LOGGER.log(Level.WARNING, "Cannot create Simplelogfile " + logFile, exception); + return false; + } + } } diff --git a/src/main/java/kst4contest/view/Kst4ContestApplication.java b/src/main/java/kst4contest/view/Kst4ContestApplication.java index 4d95b911..2f39745a 100644 --- a/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -84,6 +84,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL private static final Logger LOGGER = Logger.getLogger( Kst4ContestApplication.class.getName()); + private static final String SIMPLE_LOG_MANUAL_URL = + "https://kst4contest.hamradioonline.de/manual/en/log-sync/" + + "#method-1-universal-file-based-callsign-interpreter-simplelogfile"; private boolean gridSquareHighlightEnabled = false; @@ -12129,6 +12132,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL }); } + @Override + public void onSimpleLogFileCreated(Path filePath) { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle("Simplelogfile created"); + alert.setHeaderText("The selected Simplelogfile did not exist and has been created"); + + Label explanation = new Label( + "File: " + filePath + "\n\n" + + "Configure your logging application to write its live log to this file. " + + "Then log a test QSO and verify that the callsign is marked as worked " + + "in KST4Contest within one minute."); + explanation.setWrapText(true); + + Hyperlink manualLink = new Hyperlink("Open the Simplelogfile manual"); + manualLink.setOnAction(event -> getHostServices().showDocument(SIMPLE_LOG_MANUAL_URL)); + + VBox content = new VBox(10, explanation, manualLink); + content.setPrefWidth(560); + alert.getDialogPane().setContent(content); + alert.show(); + } + /** * Forces the station FilteredList to evaluate all active predicates again. diff --git a/src/test/java/kst4contest/controller/ChatControllerSimpleLogTest.java b/src/test/java/kst4contest/controller/ChatControllerSimpleLogTest.java new file mode 100644 index 00000000..809eaca8 --- /dev/null +++ b/src/test/java/kst4contest/controller/ChatControllerSimpleLogTest.java @@ -0,0 +1,51 @@ +package kst4contest.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; + +import kst4contest.model.ChatMember; +import kst4contest.model.ClusterMessage; +import org.junit.jupiter.api.Test; + +class ChatControllerSimpleLogTest { + + @Test + void marksEveryActiveVariantOfWorkedBaseCallsign() { + ChatMember categoryTwoVariant = member("9A0BB-2"); + ChatMember categoryThreeVariant = member("9A0BB-70"); + ChatMember unrelated = member("DL1ABC"); + + int changed = ChatController.markSimpleLogWorkedMembers( + List.of(categoryTwoVariant, categoryThreeVariant, unrelated), + Set.of("9A0BB")); + + assertEquals(2, changed); + assertTrue(categoryTwoVariant.isWorked()); + assertTrue(categoryThreeVariant.isWorked()); + assertFalse(unrelated.isWorked()); + } + + @Test + void marksClusterReceiverByBaseCallsignAndHandlesIncompleteMessages() { + ClusterMessage matching = new ClusterMessage(); + matching.setReceiver(member("9A0BB-70")); + ClusterMessage incomplete = new ClusterMessage(); + + int changed = ChatController.markSimpleLogWorkedClusterMessages( + List.of(matching, incomplete), Set.of("9A0BB")); + + assertEquals(1, changed); + assertTrue(matching.isReceiverWkd()); + assertFalse(incomplete.isReceiverWkd()); + } + + private static ChatMember member(String callSign) { + ChatMember member = new ChatMember(); + member.setCallSign(callSign); + return member; + } +} diff --git a/src/test/java/kst4contest/controller/UserActualizationTaskTest.java b/src/test/java/kst4contest/controller/UserActualizationTaskTest.java new file mode 100644 index 00000000..a01ff91e --- /dev/null +++ b/src/test/java/kst4contest/controller/UserActualizationTaskTest.java @@ -0,0 +1,76 @@ +package kst4contest.controller; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.file.Files; +import java.nio.file.Path; + +import kst4contest.model.ChatPreferences; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserActualizationTaskTest { + + @TempDir + Path temporaryDirectory; + + @Mock + ChatController controller; + + @Mock + ChatPreferences preferences; + + @BeforeEach + void configureControllerPreferences() { + when(controller.getChatPreferences()).thenReturn(preferences); + } + + @Test + void disabledInterpreterDoesNotCreateOrReadFile() { + when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(false); + + new UserActualizationTask(controller).run(); + + verify(controller, never()).applySimpleLogWorkedBaseCalls(anySet()); + verify(controller, never()).notifySimpleLogFileCreated(org.mockito.ArgumentMatchers.any()); + } + + @Test + void createsMissingFileAndNotifiesOnlyOnce() { + Path logFile = temporaryDirectory.resolve("created.log").toAbsolutePath().normalize(); + when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(true); + when(preferences.getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly()) + .thenReturn(logFile.toString()); + UserActualizationTask task = new UserActualizationTask(controller); + + task.run(); + task.run(); + + assertTrue(Files.isRegularFile(logFile)); + verify(controller, times(2)).applySimpleLogWorkedBaseCalls(anySet()); + verify(controller, times(1)).notifySimpleLogFileCreated(logFile); + } + + @Test + void readFailureIsContainedAndDoesNotUpdateUiState() { + when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(true); + when(preferences.getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly()) + .thenReturn(temporaryDirectory.toString()); + + assertDoesNotThrow(() -> new UserActualizationTask(controller).run()); + + verify(controller, never()).applySimpleLogWorkedBaseCalls(anySet()); + verify(controller, never()).notifySimpleLogFileCreated(org.mockito.ArgumentMatchers.any()); + } +} diff --git a/src/test/java/kst4contest/test/TestUCXLogFileToHashsetParser.java b/src/test/java/kst4contest/test/TestUCXLogFileToHashsetParser.java index 31f95a35..917b84ff 100644 --- a/src/test/java/kst4contest/test/TestUCXLogFileToHashsetParser.java +++ b/src/test/java/kst4contest/test/TestUCXLogFileToHashsetParser.java @@ -1,24 +1,48 @@ package kst4contest.test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; import kst4contest.controller.UCXLogFileToHashsetParser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; -public class TestUCXLogFileToHashsetParser { +class TestUCXLogFileToHashsetParser { + + @TempDir + Path temporaryDirectory; @Test - public static void main(String[] args) { - // TODO Auto-generated method stub + void parsesUniqueWorkedBaseCallsignsWithFixedPattern() throws IOException { + Path logFile = temporaryDirectory.resolve("contest.log"); + Files.writeString(logFile, String.join(System.lineSeparator(), + "QSO 001 S53CC 144 MHz", + "QSO 002 9A0BB-70 432 MHz", + "QSO 003 s53cc repeated", + "no station here")); - UCXLogFileToHashsetParser testTheParser = new UCXLogFileToHashsetParser("C:\\UcxLog\\Logs\\DO5AMF\\DVU322_I.UCX"); - try { - testTheParser.parse(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + Set result = new UCXLogFileToHashsetParser(logFile.toString()).parse(); + + assertEquals(Set.of("S53CC", "9A0BB"), result); } + @Test + void closesFileAfterEveryParsePass() throws IOException { + Path logFile = temporaryDirectory.resolve("replaceable.log"); + Files.writeString(logFile, "QSO DL1ABC"); + + UCXLogFileToHashsetParser parser = new UCXLogFileToHashsetParser(logFile.toString()); + assertEquals(Set.of("DL1ABC"), parser.parse()); + + Path replacement = temporaryDirectory.resolve("replacement.log"); + Files.move(logFile, replacement); + assertFalse(Files.exists(logFile)); + assertTrue(Files.exists(replacement)); + } }