Improvements config.serial bundle: Add JavaDoc & fix concurrency bug (#5161)

* Improvements config.serial bundle: Add JavaDoc & fix concurrency bug
* Improvements config.dispatch bundle: Add JavaDoc, fix NPE bug & add @NonNullByDefault
* Improvements ephemeris bundle: Add comprehensive JavaDoc
* Improvements id bundle: Add JavaDoc
* Improvements bin2json bundle: Add JavaDoc
* Improvements console bundle: Add JavaDoc
* Improvements console.eclipse bundle: Add JavaDoc
* Improvements console.rfc147 bundle: Add comprehensive JavaDoc

Signed-off-by: Vambot07 <salihinazizizol@gmail.com>
This commit is contained in:
Vambot07
2026-02-28 18:30:42 +01:00
committed by GitHub
parent f9fb6a1323
commit 0a1cc016bc
18 changed files with 528 additions and 16 deletions
@@ -87,6 +87,7 @@ import com.google.gson.JsonSyntaxException;
* @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);
@@ -118,17 +119,25 @@ 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);
@@ -136,6 +145,16 @@ public class ConfigDispatcher {
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);
@@ -153,6 +172,16 @@ public class ConfigDispatcher {
}
}
/**
* 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();
@@ -162,7 +191,7 @@ public class ConfigDispatcher {
}
}
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");
@@ -223,9 +252,25 @@ public class ConfigDispatcher {
}
}
/**
* 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));
@@ -393,13 +438,21 @@ public class ConfigDispatcher {
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();
}
@@ -491,7 +544,7 @@ public class ConfigDispatcher {
* 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.
@@ -510,8 +563,8 @@ public class ConfigDispatcher {
}
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
}
@@ -543,7 +596,7 @@ public class ConfigDispatcher {
.toList();
}
public boolean contains(String pid) {
public boolean contains(@Nullable String pid) {
return processedPIDMapping.containsKey(pid);
}
}
@@ -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) {
@@ -57,11 +66,25 @@ public class ConfigDispatcherFileWatcher implements WatchService.WatchEventListe
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 {
@@ -50,33 +50,96 @@ 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) {
@@ -84,6 +147,7 @@ public class SerialConfigOptionProvider implements ConfigOptionProvider, UsbSeri
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();
@@ -103,6 +103,15 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
private @NonNullByDefault({}) String country;
private @Nullable String region;
/**
* Constructs and activates the EphemerisManagerImpl.
* This constructor is called by the OSGi framework during component activation.
* It initializes the ephemeris manager with locale support, loads country/region/city descriptions
* from the Jollyday library resources, and sets up the default weekend dayset.
*
* @param localeProvider the locale provider service for internationalization support
* @param bundleContext the OSGi bundle context used to access bundle resources
*/
@Activate
public EphemerisManagerImpl(final @Reference LocaleProvider localeProvider, final BundleContext bundleContext) {
this.localeProvider = localeProvider;
@@ -125,15 +134,37 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
}
}
/**
* Sorts a list of parameter options alphabetically by their label.
* This ensures that country, region, and city options are presented in alphabetical order
* in the user interface configuration dropdowns.
*
* @param parameterOptions the list of parameter options to sort in-place
*/
private void sortByLabel(List<ParameterOption> parameterOptions) {
parameterOptions.sort(Comparator.comparing(ParameterOption::getLabel));
}
/**
* Activates the EphemerisManagerImpl component.
* This method is called by the OSGi framework when the component is activated.
* It delegates to the {@link #modified(Map)} method to process the initial configuration.
*
* @param config the initial configuration properties
*/
@Activate
protected void activate(Map<String, Object> config) {
modified(config);
}
/**
* Processes configuration updates for the EphemerisManagerImpl.
* This method is called by the OSGi framework when the configuration is modified.
* It processes custom dayset definitions (e.g., custom weekend days), and updates
* the country, region, and city parameters for holiday calculations.
*
* @param config the updated configuration properties
*/
@Modified
protected void modified(Map<String, Object> config) {
config.entrySet().stream().filter(e -> e.getKey().startsWith(CONFIG_DAYSET_PREFIX)).forEach(e -> {
@@ -222,6 +253,15 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
return null;
}
/**
* Converts a file path to a URL.
* This method checks if the file exists on the local filesystem and converts it to a file:// URL.
* Used to load custom holiday definition files from the filesystem.
*
* @param filename the absolute or relative path to the file
* @return the URL representing the file
* @throws FileNotFoundException if the file does not exist or the URL cannot be formed
*/
private URL getUrl(String filename) throws FileNotFoundException {
if (Files.exists(Path.of(filename))) {
try {
@@ -234,6 +274,14 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
}
}
/**
* Retrieves or creates a HolidayManager for the specified key.
* This method caches HolidayManager instances to avoid recreating them for repeated requests.
* The manager key can be either a country code (String) or a URL to a custom holiday definition file.
*
* @param managerKey either a country code string or a URL to a holiday configuration file
* @return the HolidayManager instance for the specified key
*/
private HolidayManager getHolidayManager(Object managerKey) {
HolidayManager holidayManager = holidayManagers.get(managerKey);
if (holidayManager == null) {
@@ -253,6 +301,16 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
return holidayManager;
}
/**
* Retrieves a list of holidays within a specified time window.
* This method queries the HolidayManager for all holidays between the start date and
* the end date (start date + span), considering the configured country/region/city parameters.
*
* @param from the start date of the time window
* @param span the number of days to look ahead from the start date
* @param holidayManager the HolidayManager instance to query
* @return a sorted list of holidays within the time window
*/
private List<Holiday> getHolidays(ZonedDateTime from, int span, HolidayManager holidayManager) {
LocalDate fromDate = from.toLocalDate();
LocalDate toDate = from.plusDays(span).toLocalDate();
@@ -276,6 +334,16 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
return getDaysUntil(from, searchedHoliday, getUrl(filename));
}
/**
* Calculates the number of days until a specified holiday.
* This method searches for the holiday within a 1-year time window starting from the given date.
* The search is case-insensitive and matches against the holiday's property key.
*
* @param from the start date to calculate from
* @param searchedHoliday the name/key of the holiday to search for
* @param holidayManager the HolidayManager instance to query
* @return the number of days until the holiday, or -1 if the holiday is not found within the year
*/
private long getDaysUntil(ZonedDateTime from, String searchedHoliday, HolidayManager holidayManager) {
List<Holiday> sortedHolidays = getHolidays(from, 366, holidayManager);
Optional<Holiday> result = sortedHolidays.stream()
@@ -283,6 +351,15 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
return result.map(holiday -> from.toLocalDate().until(holiday.getDate(), ChronoUnit.DAYS)).orElse(-1L);
}
/**
* Retrieves the key of the first bank holiday within a specified time window.
* This method searches for the earliest occurring holiday starting from the given date.
*
* @param from the start date of the time window
* @param span the number of days to search ahead
* @param holidayManager the HolidayManager instance to query
* @return the property key of the first holiday found, or null if no holiday exists in the time window
*/
private @Nullable String getFirstBankHolidayKey(ZonedDateTime from, int span, HolidayManager holidayManager) {
Optional<Holiday> holiday = getHolidays(from, span, holidayManager).stream().findFirst();
return holiday.map(Holiday::getPropertiesKey).orElse(null);
@@ -350,6 +427,16 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
return getNextBankHoliday(from, getUrl(filename));
}
/**
* Adds or updates a dayset definition.
* A dayset is a collection of days of the week (e.g., weekend = Saturday + Sunday).
* This method parses the provided values, strips non-alphabetic characters, converts to uppercase,
* and creates a set of DayOfWeek values.
*
* @param setName the name of the dayset (e.g., "weekend", "workdays")
* @param values an iterable collection of day names (e.g., "SATURDAY", "SUNDAY")
* @throws IllegalArgumentException if a day name cannot be parsed as a valid DayOfWeek
*/
private void addDayset(String setName, Iterable<?> values) {
Set<DayOfWeek> dayset = new HashSet<>();
for (Object day : values) {
@@ -421,6 +508,15 @@ public class EphemerisManagerImpl implements EphemerisManager, ConfigOptionProvi
}
}
/**
* Validates and normalizes a property part.
* This method trims whitespace and converts the part to lowercase.
* Used during parsing of country/region/city property keys.
*
* @param part the property part to validate
* @return the trimmed and lowercased property part
* @throws IllegalArgumentException if the part is empty after trimming
*/
private static String getValidPart(String part) {
final String subject = part.trim();
if (!subject.isEmpty()) {
@@ -71,6 +71,15 @@ public class InstanceUUID {
return uuid;
}
/**
* Generates a new UUID and writes it to the specified file.
* This method creates any necessary parent directories and writes the UUID to the file
* using UTF-8 encoding.
*
* @param file the file where the UUID will be stored
* @return the newly generated UUID string
* @throws IOException if the file cannot be written
*/
private static String generateToFile(File file) throws IOException {
// create intermediary directories
if (file.getParentFile() instanceof File parentFile) {
@@ -81,6 +90,14 @@ public class InstanceUUID {
return newUuid;
}
/**
* Reads the first line from the UUID file.
* This method opens the file, reads the first line, and properly closes the reader.
* If the file cannot be read or is empty, an empty string is returned and a warning is logged.
*
* @param file the file to read from
* @return the first line of the file, or an empty string if the file cannot be read or is empty
*/
private static String readFirstLine(File file) {
try (final BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
return reader.readLine() instanceof String line ? line : "";
@@ -55,6 +55,13 @@ public class UUIDResource implements RESTResource {
public static final String PATH_UUID = "uuid";
/**
* Retrieves the instance UUID via REST endpoint.
* This method exposes the unique instance identifier through a REST API endpoint.
* The UUID is generated once and persisted, remaining constant across restarts.
*
* @return a Response containing the instance UUID as plain text, or null if the UUID cannot be retrieved
*/
@GET
@Produces(MediaType.TEXT_PLAIN)
@Operation(operationId = "getUUID", summary = "A unified unique id.", responses = {
@@ -137,6 +137,15 @@ public class Bin2Json {
}
}
/**
* Converts a parsed JBBP field structure to a JSON object.
* This internal method performs the actual conversion of parsed binary data to JSON format,
* tracking the conversion time when trace logging is enabled.
*
* @param data the parsed JBBP field structure containing binary data
* @return Gson {@link JsonObject} representation of the binary data
* @throws ConversionException if an error occurs during the JSON conversion process
*/
private JsonObject convert(JBBPFieldStruct data) throws ConversionException {
try {
LocalDateTime start = LocalDateTime.now();
@@ -151,10 +160,30 @@ public class Bin2Json {
}
}
/**
* Converts a JBBP field to a new JSON object.
* This is a convenience wrapper method that delegates to the main conversion method with a null
* initial JSON object, resulting in a new JSON object being created.
*
* @param field the JBBP field to convert to JSON
* @return a new Gson {@link JsonObject} containing the field data
* @throws ConversionException if the field type is not supported or conversion fails
*/
private JsonObject convertToJSon(final JBBPAbstractField field) throws ConversionException {
return convertToJSon(null, field);
}
/**
* Converts a JBBP field to JSON, optionally adding to an existing JSON object.
* This is the main conversion method that recursively handles all JBBP field types including
* primitives (bit, boolean, byte, int, long, short), unsigned types (ubyte, ushort),
* arrays, and nested structures. Each field is added to the JSON object with its field name as the key.
*
* @param json the existing JSON object to add the field to, or null to create a new JSON object
* @param field the JBBP field to convert (can be primitive, array, or struct)
* @return the JSON object containing the converted field data
* @throws ConversionException if the field type is not recognized or supported
*/
private JsonObject convertToJSon(@Nullable final JsonObject json, final JBBPAbstractField field)
throws ConversionException {
JsonObject jsn = json == null ? new JsonObject() : json;
@@ -27,17 +27,36 @@ public class ConversionException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructs a new ConversionException with no detail message.
*/
public ConversionException() {
}
/**
* Constructs a new ConversionException with the specified detail message.
*
* @param message the detail message describing the conversion error
*/
public ConversionException(String message) {
super(message);
}
/**
* Constructs a new ConversionException with the specified detail message and cause.
*
* @param message the detail message describing the conversion error
* @param cause the underlying cause of the conversion error
*/
public ConversionException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new ConversionException with the specified cause.
*
* @param cause the underlying cause of the conversion error
*/
public ConversionException(Throwable cause) {
super(cause);
}
@@ -46,22 +46,48 @@ public class ConsoleSupportEclipse implements CommandProvider {
private final SortedMap<String, ConsoleCommandExtension> consoleCommandExtensions = Collections
.synchronizedSortedMap(new TreeMap<>());
/**
* Constructs a new ConsoleSupportEclipse instance.
*/
public ConsoleSupportEclipse() {
}
/**
* Adds a console command extension to the registry.
* This method is called dynamically by OSGi when a new console command extension is registered.
*
* @param consoleCommandExtension the console command extension to add
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void addConsoleCommandExtension(ConsoleCommandExtension consoleCommandExtension) {
consoleCommandExtensions.put(consoleCommandExtension.getCommand(), consoleCommandExtension);
}
/**
* Removes a console command extension from the registry.
* This method is called dynamically by OSGi when a console command extension is unregistered.
*
* @param consoleCommandExtension the console command extension to remove
*/
public void removeConsoleCommandExtension(ConsoleCommandExtension consoleCommandExtension) {
consoleCommandExtensions.remove(consoleCommandExtension.getCommand());
}
/**
* Gets a console command extension by command name.
*
* @param cmd the command name
* @return the console command extension, or null if not found
*/
private ConsoleCommandExtension getConsoleCommandExtension(final String cmd) {
return consoleCommandExtensions.get(cmd);
}
/**
* Gets all registered console command extensions.
*
* @return a collection of all console command extensions
*/
private Collection<ConsoleCommandExtension> getConsoleCommandExtensions() {
return new HashSet<>(consoleCommandExtensions.values());
}
@@ -105,6 +131,11 @@ public class ConsoleSupportEclipse implements CommandProvider {
return null;
}
/**
* Gets the help text for all registered openHAB console commands.
*
* @return the help text listing all available commands and their usage
*/
@Override
public String getHelp() {
return ConsoleInterpreter.getHelp(BASE, " ", getConsoleCommandExtensions());
@@ -17,6 +17,8 @@ import org.eclipse.osgi.framework.console.CommandInterpreter;
import org.openhab.core.io.console.Console;
/**
* Implementation of the Console interface for Eclipse OSGi console.
* This class wraps the Eclipse OSGi CommandInterpreter to provide a unified console interface.
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Split to separate file
@@ -27,21 +29,43 @@ public class OSGiConsole implements Console {
private final String baseCommand;
private final CommandInterpreter interpreter;
/**
* Constructs a new OSGi console wrapper.
*
* @param baseCommand the base command name (e.g., "openhab")
* @param interpreter the Eclipse OSGi command interpreter
*/
public OSGiConsole(final String baseCommand, final CommandInterpreter interpreter) {
this.baseCommand = baseCommand;
this.interpreter = interpreter;
}
/**
* Prints a string to the console without a newline.
*
* @param s the string to print
*/
@Override
public void print(final String s) {
interpreter.print(s);
}
/**
* Prints a string to the console with a newline.
*
* @param s the string to print
*/
@Override
public void println(final String s) {
interpreter.println(s);
}
/**
* Prints the usage information for a command.
* The output format is: "Usage: {baseCommand} {usageString}".
*
* @param s the usage string describing command syntax
*/
@Override
public void printUsage(final String s) {
interpreter.println(String.format("Usage: %s %s", baseCommand, s));
@@ -19,6 +19,9 @@ import org.openhab.core.io.console.ConsoleInterpreter;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
/**
* Wrapper class that bridges console command extensions to OSGi RFC 147 command interface.
* This class wraps a ConsoleCommandExtension and exposes it as an OSGi command that can be
* invoked through the OSGi command shell.
*
* @author Markus Rathgeb - Initial contribution
*/
@@ -27,14 +30,23 @@ public class CommandWrapper {
private final ConsoleCommandExtension command;
/**
* Constructs a new command wrapper.
*
* @param command the console command extension to wrap
*/
public CommandWrapper(final ConsoleCommandExtension command) {
this.command = command;
}
// Called for all commands there is no function found for.
// The first argument is the command.
// The CommandSession interface provides methods for executing commands and getting and setting session variables.
// We could return an Object or void.
/**
* Main entry point for command execution via OSGi RFC 147 interface.
* This method is called when no specific function is found for the command.
* The first argument is the command name, and remaining arguments are passed to the command.
*
* @param args the command arguments, where args[0] is the command name
* @throws Exception if command execution fails
*/
public void _main(/* CommandSession session, */String[] args) throws Exception {
if (args.length == 0) {
System.out.println("missing command");
@@ -18,11 +18,19 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
/**
* Container interface for accessing registered console command extensions.
* This interface provides access to all console commands that are currently registered
* in the OSGi RFC 147 console support.
*
* @author Markus Rathgeb - Initial contribution
*/
@NonNullByDefault
public interface ConsoleCommandsContainer {
/**
* Gets all registered console command extensions.
*
* @return a collection of all console command extensions currently registered
*/
Collection<ConsoleCommandExtension> getConsoleCommandExtensions();
}
@@ -73,11 +73,21 @@ public class ConsoleSupportRfc147 implements ConsoleCommandsContainer {
private final Map<ConsoleCommandExtension, @Nullable ServiceRegistration<?>> commands = Collections
.synchronizedMap(new HashMap<>());
/**
* Constructs a new RFC 147 console support instance.
* Initializes the commands map and registers the built-in help command.
*/
public ConsoleSupportRfc147() {
// Add our custom help console command extensions.
commands.put(helpCommand, null);
}
/**
* Activates this OSGi component.
* Registers all pending console command extensions as OSGi services and activates the help command.
*
* @param ctx the component context provided by OSGi
*/
@Activate
public void activate(ComponentContext ctx) {
// Save bundle context to register services.
@@ -97,6 +107,10 @@ public class ConsoleSupportRfc147 implements ConsoleCommandsContainer {
helpCommand.setConsoleCommandsContainer(this);
}
/**
* Deactivates this OSGi component.
* Unregisters all console command extensions and clears the help command reference.
*/
@Deactivate
public void deactivate() {
// If we get deactivated, remove from help command (so GC could do their work).
@@ -117,6 +131,13 @@ public class ConsoleSupportRfc147 implements ConsoleCommandsContainer {
this.bundleContext = null;
}
/**
* Adds a console command extension to the registry.
* This method is called dynamically by OSGi when a new console command extension is registered.
* The command is immediately registered as an OSGi service if the component is active.
*
* @param consoleCommandExtension the console command extension to add
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void addConsoleCommandExtension(ConsoleCommandExtension consoleCommandExtension) {
final ServiceRegistration<?> old;
@@ -127,6 +148,13 @@ public class ConsoleSupportRfc147 implements ConsoleCommandsContainer {
}
}
/**
* Removes a console command extension from the registry.
* This method is called dynamically by OSGi when a console command extension is unregistered.
* The command's OSGi service registration is also removed.
*
* @param consoleCommandExtension the console command extension to remove
*/
public void removeConsoleCommandExtension(ConsoleCommandExtension consoleCommandExtension) {
final ServiceRegistration<?> old;
@@ -136,6 +164,11 @@ public class ConsoleSupportRfc147 implements ConsoleCommandsContainer {
}
}
/**
* Creates an empty properties dictionary for OSGi service registration.
*
* @return an empty dictionary that can be used to store service properties
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private Dictionary<String, Object> createProperties() {
return (Dictionary) new Properties();
@@ -16,6 +16,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.console.Console;
/**
* Implementation of the Console interface for OSGi RFC 147 console.
* This console implementation outputs to System.out and uses a base command scope
* for formatting usage messages.
*
* @author Markus Rathgeb - Initial contribution
*/
@@ -24,10 +27,20 @@ public class OSGiConsole implements Console {
private final String base;
/**
* Constructs a new OSGi RFC 147 console.
*
* @param base the base command scope (e.g., "openhab")
*/
public OSGiConsole(final String base) {
this.base = base;
}
/**
* Gets the base command scope.
*
* @return the base command scope string
*/
public String getBase() {
return base;
}
@@ -23,6 +23,9 @@ import org.openhab.core.io.console.rfc147.internal.ConsoleCommandsContainer;
import org.openhab.core.io.console.rfc147.internal.ConsoleSupportRfc147;
/**
* Console command extension that provides help information for all available commands.
* This command displays usage information for all registered console commands in the
* OSGi RFC 147 console.
*
* @author Markus Rathgeb - Initial contribution
*/
@@ -31,15 +34,30 @@ public class HelpConsoleCommandExtension extends AbstractConsoleCommandExtension
private @Nullable ConsoleCommandsContainer commandsContainer;
/**
* Constructs a new help console command extension.
* Registers the "help" command with a description.
*/
public HelpConsoleCommandExtension() {
super("help", "Get help for all available commands.");
}
/**
* Sets the container that provides access to all registered console commands.
* This is used to retrieve the list of commands to display help for.
*
* @param commandsContainer the commands container, or null to clear the reference
*/
public void setConsoleCommandsContainer(final @Nullable ConsoleCommandsContainer commandsContainer) {
this.commandsContainer = commandsContainer;
}
// Add a method that name is equal to our command
/**
* Command entry point that matches the command name "help".
* This method is invoked by the OSGi RFC 147 command infrastructure when the help command is called.
*
* @param args the command arguments
*/
public void help(String[] args) {
execute(args, ConsoleSupportRfc147.CONSOLE);
}
@@ -26,12 +26,30 @@ import org.eclipse.jdt.annotation.Nullable;
@NonNullByDefault
public interface Console {
/**
* Prints formatted output to the console.
* This method formats the given string using the specified arguments and prints it to the console
* without appending a newline.
*
* @param format the format string (following {@link String#format} syntax)
* @param args the arguments referenced by the format specifiers in the format string
*/
default void printf(String format, Object... args) {
print(String.format(format, args));
}
/**
* Prints a string to the console without appending a newline.
*
* @param s the string to print
*/
void print(String s);
/**
* Prints a string to the console followed by a newline.
*
* @param s the string to print
*/
void println(String s);
/**
@@ -30,6 +30,16 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class ConsoleInterpreter {
/**
* Generates a formatted help message listing all available console commands.
* This method creates a comprehensive help text that includes all command extensions
* with their usage information, formatted with the specified base command and separator.
*
* @param base the base command string (e.g., "openhab")
* @param separator the separator between base and command (e.g., ":")
* @param extensions the collection of console command extensions to include in the help
* @return a formatted string containing all command usages
*/
public static String getHelp(final String base, final String separator,
Collection<ConsoleCommandExtension> extensions) {
final List<String> usages = ConsoleInterpreter.getUsages(extensions);
@@ -47,11 +57,30 @@ public class ConsoleInterpreter {
return buffer.toString();
}
/**
* Prints the formatted help message to the console.
* This is a convenience method that generates and prints the help text for all
* available console commands.
*
* @param console the console to print the help message to
* @param base the base command string (e.g., "openhab")
* @param separator the separator between base and command (e.g., ":")
* @param extensions the collection of console command extensions to include in the help
*/
public static void printHelp(final Console console, final String base, final String separator,
Collection<ConsoleCommandExtension> extensions) {
console.println(getHelp(base, separator, extensions));
}
/**
* Executes a console command extension with the given arguments.
* This method wraps the execution with error handling, logging any exceptions that occur
* and providing user-friendly error messages to the console.
*
* @param console the console for command output
* @param extension the console command extension to execute
* @param args the arguments to pass to the command
*/
public static void execute(Console console, ConsoleCommandExtension extension, String[] args) {
try {
extension.execute(args, console);
@@ -62,7 +91,14 @@ public class ConsoleInterpreter {
}
}
/** returns a CR-separated list of usage texts for all available commands */
/**
* Returns a newline-separated list of usage texts for all available commands.
* Each usage text is on its own line, providing a complete list of all command
* usages from the given console command extensions.
*
* @param consoleCommandExtensions the collection of console command extensions
* @return a newline-separated string containing all command usage texts
*/
public static String getUsage(Collection<ConsoleCommandExtension> consoleCommandExtensions) {
StringBuilder sb = new StringBuilder();
for (String usage : ConsoleInterpreter.getUsages(consoleCommandExtensions)) {
@@ -71,7 +107,14 @@ public class ConsoleInterpreter {
return sb.toString();
}
/** returns an array of the usage texts for all available commands */
/**
* Returns a list of usage texts for all available commands.
* This method collects all usage strings from the given console command extensions
* and returns them as a list.
*
* @param consoleCommandExtensions the collection of console command extensions
* @return a list containing all command usage texts
*/
public static List<String> getUsages(Collection<ConsoleCommandExtension> consoleCommandExtensions) {
List<String> usages = new ArrayList<>();
for (ConsoleCommandExtension consoleCommandExtension : consoleCommandExtensions) {
@@ -31,6 +31,10 @@ public class StringsCompleter implements ConsoleCommandCompleter {
private final SortedSet<String> strings;
private final boolean caseSensitive;
/**
* Creates a case-insensitive StringsCompleter with an empty set of strings.
* Strings can be added later by modifying the set returned from {@link #getStrings()}.
*/
public StringsCompleter() {
this(List.of(), false);
}