Skip to content
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 @@ -87,6 +87,7 @@
* @author Christoph Weitkamp - Added support for value containing a list of configuration options
*/
@Component(immediate = true, service = ConfigDispatcher.class)
@NonNullByDefault
public class ConfigDispatcher {

private final Logger logger = LoggerFactory.getLogger(ConfigDispatcher.class);
Expand Down Expand Up @@ -118,24 +119,42 @@ public class ConfigDispatcher {
private static final String DEFAULT_LIST_ENDING_CHARACTER = "]";
private static final String DEFAULT_LIST_DELIMITER = ",";

private ExclusivePIDMap exclusivePIDMap;
private @Nullable ExclusivePIDMap exclusivePIDMap;

private final ConfigurationAdmin configAdmin;

private File exclusivePIDStore;
private @Nullable File exclusivePIDStore;

@Activate
public ConfigDispatcher(final @Reference ConfigurationAdmin configAdmin) {
this.configAdmin = configAdmin;
}

/**
* Activates the ConfigDispatcher component.
* This method is called by the OSGi framework when the component is activated.
* It initializes the exclusive PID store, loads any previously saved exclusive PIDs,
* and processes the default configuration file.
*
* @param bundleContext the OSGi bundle context used to access bundle-specific data files
*/
@Activate
public void activate(BundleContext bundleContext) {
exclusivePIDStore = bundleContext.getDataFile(EXCLUSIVE_PID_STORE_FILE);
loadExclusivePIDList();
readDefaultConfig();
}

/**
* Loads the list of exclusive PIDs from the bundle data file.
* Exclusive PIDs are configuration PIDs that are managed by a single configuration file
* (marked with "pid:" prefix). This method attempts to deserialize the previously stored
* PID list from JSON format. If the file doesn't exist or cannot be parsed, a new empty
* map is created.
*
* <p>
* This method is called during component activation to restore the state from previous runs.
*/
private void loadExclusivePIDList() {
try (FileReader reader = new FileReader(exclusivePIDStore)) {
exclusivePIDMap = gson.fromJson(reader, ExclusivePIDMap.class);
Expand All @@ -153,6 +172,16 @@ private void loadExclusivePIDList() {
}
}

/**
* Stores the current list of exclusive PIDs to the bundle data file.
* This method serializes the exclusive PID map to JSON format and writes it to persistent storage.
* The stored data is used to track which configuration files use exclusive PIDs and to detect
* orphaned configurations when files are deleted.
*
* <p>
* This method is called after processing configuration files to ensure the state is preserved
* across component restarts.
*/
private void storeCurrentExclusivePIDList() {
try (FileWriter writer = new FileWriter(exclusivePIDStore)) {
exclusivePIDMap.setCurrentExclusivePIDList();
Expand All @@ -162,7 +191,7 @@ private void storeCurrentExclusivePIDList() {
}
}

private Configuration getConfigurationWithContext(String pidWithContext)
private @Nullable Configuration getConfigurationWithContext(String pidWithContext)
throws IOException, InvalidSyntaxException {
if (!pidWithContext.contains(OpenHAB.SERVICE_CONTEXT_MARKER)) {
throw new IllegalArgumentException("Given PID should be followed by a context");
Expand Down Expand Up @@ -223,9 +252,25 @@ private void readDefaultConfig() {
}
}

/**
* Processes configuration files from a directory or a single file.
* If the given file is a directory, all .cfg files within it are processed in order of
* their last modification time (oldest first). If the given file is a regular file,
* it is processed directly.
*
* <p>
* After processing all files, this method cleans up orphaned exclusive PIDs
* (configurations whose files have been deleted) and saves the current state.
*
* @param dir the directory containing configuration files, or a single configuration file to process
*/
public void processConfigFile(File dir) {
if (dir.isDirectory() && dir.exists()) {
File[] files = dir.listFiles();
if (files == null) {
logger.warn("Unable to list files in directory '{}', skipping processing", dir.getAbsolutePath());
return;
}
// Sort the files by modification time,
// so that the last modified file is processed last.
Arrays.sort(files, Comparator.comparingLong(File::lastModified));
Expand Down Expand Up @@ -393,13 +438,21 @@ private void internalProcessConfigFile(File configFile) throws IOException {
storeCurrentExclusivePIDList();
}

/**
* Called when a configuration file is removed.
* This method marks the configuration file as removed in the exclusive PID map,
* processes any orphaned PIDs (configurations whose files no longer exist),
* and updates the persisted state.
*
* @param path the absolute path of the removed configuration file
*/
public void fileRemoved(String path) {
exclusivePIDMap.setFileRemoved(path);
processOrphanExclusivePIDs();
storeCurrentExclusivePIDList();
}

private String getPIDFromLine(String line) {
private @Nullable String getPIDFromLine(String line) {
if (line.startsWith(PID_MARKER)) {
return line.substring(PID_MARKER.length()).trim();
}
Expand Down Expand Up @@ -491,7 +544,7 @@ public static class ExclusivePIDMap {
* service config files.
* The map will hold a 1:1 relation mapping from an exclusive PID to its absolute path in the file system.
*/
private transient Map<String, String> processedPIDMapping = new HashMap<>();
private transient Map<String, @Nullable String> processedPIDMapping = new HashMap<>();

/**
* Package protected default constructor to allow reflective instantiation.
Expand All @@ -510,8 +563,8 @@ public void removeExclusivePID(String pid) {
}

public void setFileRemoved(String absolutePath) {
for (Entry<String, String> entry : processedPIDMapping.entrySet()) {
if (entry.getValue().equals(absolutePath)) {
for (Entry<String, @Nullable String> entry : processedPIDMapping.entrySet()) {
if (absolutePath.equals(entry.getValue())) {
entry.setValue(null);
return; // we expect a 1:1 relation between PID and path
}
Expand Down Expand Up @@ -543,7 +596,7 @@ public void setCurrentExclusivePIDList() {
.toList();
}

public boolean contains(String pid) {
public boolean contains(@Nullable String pid) {
return processedPIDMapping.containsKey(pid);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public class ConfigDispatcherFileWatcher implements WatchService.WatchEventListe
private final ConfigDispatcher configDispatcher;
private final WatchService watchService;

/**
* Creates and activates the ConfigDispatcherFileWatcher.
* This constructor is called by the OSGi framework during component activation.
* It registers this component as a file system watch listener for the services configuration
* directory and performs an initial processing of all existing configuration files.
*
* @param configDispatcher the ConfigDispatcher service used to process configuration files
* @param watchService the WatchService used to monitor file system changes in the configuration directory
*/
@Activate
public ConfigDispatcherFileWatcher(final @Reference ConfigDispatcher configDispatcher,
final @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
Expand All @@ -57,11 +66,25 @@ public ConfigDispatcherFileWatcher(final @Reference ConfigDispatcher configDispa
configDispatcher.processConfigFile(Path.of(OpenHAB.getConfigFolder(), servicesFolder).toFile());
}

/**
* Deactivates the ConfigDispatcherFileWatcher.
* This method is called by the OSGi framework during component deactivation.
* It unregisters this component from the watch service to stop receiving file system events.
*/
@Deactivate
public void deactivate() {
watchService.unregisterListener(this);
}

/**
* Processes file system watch events for configuration files.
* This method is called by the WatchService when a file is created, modified, or deleted
* in the monitored services directory. It filters events to process only .cfg files that
* are not hidden, and delegates the actual processing to the ConfigDispatcher.
*
* @param kind the type of file system event (CREATE, MODIFY, or DELETE)
* @param fullPath the full path to the file that triggered the event
*/
@Override
public void processWatchEvent(WatchService.Kind kind, Path fullPath) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,40 +50,104 @@ public class SerialConfigOptionProvider implements ConfigOptionProvider, UsbSeri
private final Set<UsbSerialDeviceInformation> previouslyDiscovered = new CopyOnWriteArraySet<>();
private final Set<UsbSerialDiscovery> usbSerialDiscoveries = new CopyOnWriteArraySet<>();

/**
* Creates a new SerialConfigOptionProvider.
* This constructor is called by the OSGi framework during component activation.
*
* @param serialPortManager the serial port manager service used to retrieve available serial ports
*/
@Activate
public SerialConfigOptionProvider(final @Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}

/**
* Dynamically adds a USB serial discovery service.
* This method is called by the OSGi framework when a new {@link UsbSerialDiscovery} service becomes available.
* The discovery service is registered as a listener to receive notifications about USB serial device
* additions and removals.
*
* <p>
* This method is synchronized to prevent race conditions with {@link #removeUsbSerialDiscovery(UsbSerialDiscovery)}
* when services are dynamically bound and unbound.
*
* @param usbSerialDiscovery the USB serial discovery service to add (must not be null)
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
protected synchronized void addUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
usbSerialDiscoveries.add(usbSerialDiscovery);
usbSerialDiscovery.registerDiscoveryListener(this);
}

/**
* Dynamically removes a USB serial discovery service.
* This method is called by the OSGi framework when a {@link UsbSerialDiscovery} service becomes unavailable.
* The discovery service is unregistered as a listener and removed from the active discovery set.
*
* <p>
* <b>Note:</b> This method clears all previously discovered USB serial devices, not just those
* discovered by this specific service. This ensures a clean state when discovery services are
* dynamically removed and re-added, preventing stale device information.
*
* <p>
* This method is synchronized to prevent race conditions with {@link #addUsbSerialDiscovery(UsbSerialDiscovery)}
* when services are dynamically bound and unbound.
*
* @param usbSerialDiscovery the USB serial discovery service to remove (must not be null)
*/
protected synchronized void removeUsbSerialDiscovery(UsbSerialDiscovery usbSerialDiscovery) {
usbSerialDiscovery.unregisterDiscoveryListener(this);
usbSerialDiscoveries.remove(usbSerialDiscovery);
previouslyDiscovered.clear();
}

/**
* Called when a USB serial device is discovered.
* This method is invoked by {@link UsbSerialDiscovery} services when they detect a new USB serial device.
* The discovered device is added to the internal cache and will be included in the parameter options
* returned by {@link #getParameterOptions(URI, String, String, Locale)}.
*
* @param usbSerialDeviceInformation information about the discovered USB serial device
*/
@Override
public void usbSerialDeviceDiscovered(UsbSerialDeviceInformation usbSerialDeviceInformation) {
previouslyDiscovered.add(usbSerialDeviceInformation);
}

/**
* Called when a USB serial device is removed.
* This method is invoked by {@link UsbSerialDiscovery} services when they detect that a USB serial device
* has been disconnected. The device is removed from the internal cache and will no longer be included
* in the parameter options.
*
* @param usbSerialDeviceInformation information about the removed USB serial device
*/
@Override
public void usbSerialDeviceRemoved(UsbSerialDeviceInformation usbSerialDeviceInformation) {
previouslyDiscovered.remove(usbSerialDeviceInformation);
}

/**
* Provides serial port names as configuration parameter options.
* This method is called by the configuration framework to populate serial port selection dropdowns
* in the UI. It combines serial ports from both the {@link SerialPortManager} and any USB serial
* devices discovered through {@link UsbSerialDiscovery} services.
*
* @param uri the URI of the configuration (not used in this implementation)
* @param param the parameter name (not used in this implementation)
* @param context the parameter context; returns serial port options only if context equals "serial-port"
* @param locale the locale for internationalization (not used in this implementation)
* @return a collection of parameter options containing available serial port names,
* or {@code null} if the context is not "serial-port"
*/
@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
@Nullable Locale locale) {
if (SERIAL_PORT.equals(context)) {
return Stream
.concat(serialPortManager.getIdentifiers().map(SerialPortIdentifier::getName),
previouslyDiscovered.stream().map(UsbSerialDeviceInformation::getSerialPort))
.filter(serialPortName -> serialPortName != null && !serialPortName.isEmpty()) //
.distinct() //
.map(serialPortName -> new ParameterOption(serialPortName, serialPortName)) //
.toList();
Expand Down
Loading