Skip to content

Commit 2b4a349

Browse files
Refactor USB device classes to reduce duplication (#3122)
* Refactor USB device classes to reduce duplication * Address coderabbit review: fix flat-list controller exclusion, javadoc, catch-all controller comments, null guards, deduplication, and changelog * Configure spotless import ordering: static, java, javax, org, com, oshi * Typo/grammar Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent b88ab7f commit 2b4a349

18 files changed

Lines changed: 538 additions & 539 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [#3117](https://github.com/oshi/oshi/pull/3117),
66
[#3120](https://github.com/oshi/oshi/pull/3120),
77
[#3121](https://github.com/oshi/oshi/pull/3121): Port Linux JNA classes to FFM - [@dbwiddis](https://github.com/dbwiddis).
8+
* [#3122](https://github.com/oshi/oshi/pull/3122): Refactor USB device classes to reduce duplication - [@dbwiddis](https://github.com/dbwiddis).
89

910
# 6.11.0 (2026-04-04)
1011

oshi-core-java25/src/main/java/oshi/ffm/linux/LinuxLibcFunctions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.lang.foreign.SymbolLookup;
1919
import java.lang.invoke.MethodHandle;
2020
import java.lang.invoke.VarHandle;
21+
import java.util.Locale;
2122

2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
@@ -42,7 +43,7 @@ private LinuxLibcFunctions() {
4243
private static final long SYS_GETTID;
4344

4445
static {
45-
String arch = System.getProperty("os.arch", "").toLowerCase();
46+
String arch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT);
4647
if (arch.contains("aarch64") || arch.contains("arm64") || arch.contains("riscv64")
4748
|| arch.contains("loongarch64")) {
4849
SYS_GETTID = 178L;

oshi-core-java25/src/main/java/oshi/hardware/platform/linux/LinuxUsbDeviceFFM.java

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,29 @@
44
*/
55
package oshi.hardware.platform.linux;
66

7+
import static java.util.Collections.emptyList;
78
import static oshi.software.os.linux.LinuxOperatingSystemFFM.HAS_UDEV;
89

910
import java.lang.foreign.Arena;
1011
import java.lang.foreign.MemorySegment;
1112
import java.util.ArrayList;
12-
import java.util.Collections;
1313
import java.util.HashMap;
1414
import java.util.List;
1515
import java.util.Map;
1616

1717
import org.slf4j.Logger;
1818
import org.slf4j.LoggerFactory;
1919

20-
import oshi.annotation.concurrent.Immutable;
2120
import oshi.ffm.linux.UdevFunctions;
2221
import oshi.hardware.UsbDevice;
23-
import oshi.hardware.common.AbstractUsbDevice;
2422

2523
/**
26-
* FFM-based Linux USB device implementation.
24+
* Linux USB device helper using FFM/udev. Instantiates {@link LinuxUsbDevice} objects.
2725
*/
28-
@Immutable
29-
public class LinuxUsbDeviceFFM extends AbstractUsbDevice {
26+
public final class LinuxUsbDeviceFFM extends LinuxUsbDevice {
27+
28+
private LinuxUsbDeviceFFM() {
29+
}
3030

3131
private static final Logger LOG = LoggerFactory.getLogger(LinuxUsbDeviceFFM.class);
3232

@@ -38,38 +38,30 @@ public class LinuxUsbDeviceFFM extends AbstractUsbDevice {
3838
private static final String ATTR_PRODUCT_ID = "idProduct";
3939
private static final String ATTR_SERIAL = "serial";
4040

41-
public LinuxUsbDeviceFFM(String name, String vendor, String vendorId, String productId, String serialNumber,
42-
String uniqueDeviceId, List<UsbDevice> connectedDevices) {
43-
super(name, vendor, vendorId, productId, serialNumber, uniqueDeviceId, connectedDevices);
44-
}
45-
4641
/**
4742
* Instantiates a list of {@link oshi.hardware.UsbDevice} objects, representing devices connected via a usb port
4843
* (including internal devices).
4944
*
50-
* @param tree If true, returns a list of controllers with their device tree. If false, returns a flat list of all
51-
* devices (including controllers) with no nested connectedDevices.
45+
* @param tree If true, returns a list of controllers with their device tree. If false, returns a flat list of
46+
* devices excluding controllers.
5247
* @return a list of {@link oshi.hardware.UsbDevice} objects.
5348
*/
5449
public static List<UsbDevice> getUsbDevices(boolean tree) {
55-
List<UsbDevice> devices = getUsbDevices();
50+
List<UsbDevice> devices = queryUsbDevices();
5651
if (tree) {
5752
return devices;
5853
}
5954
List<UsbDevice> deviceList = new ArrayList<>();
6055
for (UsbDevice device : devices) {
61-
deviceList.add(new LinuxUsbDeviceFFM(device.getName(), device.getVendor(), device.getVendorId(),
62-
device.getProductId(), device.getSerialNumber(), device.getUniqueDeviceId(),
63-
Collections.emptyList()));
6456
addDevicesToList(deviceList, device.getConnectedDevices());
6557
}
6658
return deviceList;
6759
}
6860

69-
private static List<UsbDevice> getUsbDevices() {
61+
private static List<UsbDevice> queryUsbDevices() {
7062
if (!HAS_UDEV) {
7163
LOG.warn("USB Device information requires libudev, which is not present.");
72-
return Collections.emptyList();
64+
return emptyList();
7365
}
7466
List<String> usbControllers = new ArrayList<>();
7567
Map<String, String> nameMap = new HashMap<>();
@@ -82,7 +74,7 @@ private static List<UsbDevice> getUsbDevices() {
8274
try (Arena arena = Arena.ofConfined()) {
8375
MemorySegment udev = UdevFunctions.udev_new();
8476
if (MemorySegment.NULL.equals(udev)) {
85-
return Collections.emptyList();
77+
return emptyList();
8678
}
8779
try {
8880
MemorySegment enumerate = UdevFunctions.udev_enumerate_new(udev);
@@ -151,7 +143,7 @@ private static List<UsbDevice> getUsbDevices() {
151143
}
152144
} catch (Throwable e) {
153145
LOG.warn("Error enumerating USB devices: {}", e.getMessage());
154-
return Collections.emptyList();
146+
return emptyList();
155147
}
156148

157149
List<UsbDevice> controllerDevices = new ArrayList<>();
@@ -161,28 +153,4 @@ private static List<UsbDevice> getUsbDevices() {
161153
}
162154
return controllerDevices;
163155
}
164-
165-
private static void addDevicesToList(List<UsbDevice> deviceList, List<UsbDevice> list) {
166-
for (UsbDevice device : list) {
167-
deviceList.add(device);
168-
addDevicesToList(deviceList, device.getConnectedDevices());
169-
}
170-
}
171-
172-
private static LinuxUsbDeviceFFM getDeviceAndChildren(String devPath, String vid, String pid,
173-
Map<String, String> nameMap, Map<String, String> vendorMap, Map<String, String> vendorIdMap,
174-
Map<String, String> productIdMap, Map<String, String> serialMap, Map<String, List<String>> hubMap) {
175-
String vendorId = vendorIdMap.getOrDefault(devPath, vid);
176-
String productId = productIdMap.getOrDefault(devPath, pid);
177-
List<String> childPaths = hubMap.getOrDefault(devPath, new ArrayList<>());
178-
List<UsbDevice> usbDevices = new ArrayList<>();
179-
for (String path : childPaths) {
180-
usbDevices.add(getDeviceAndChildren(path, vendorId, productId, nameMap, vendorMap, vendorIdMap,
181-
productIdMap, serialMap, hubMap));
182-
}
183-
Collections.sort(usbDevices);
184-
return new LinuxUsbDeviceFFM(nameMap.getOrDefault(devPath, vendorId + ":" + productId),
185-
vendorMap.getOrDefault(devPath, ""), vendorId, productId, serialMap.getOrDefault(devPath, ""), devPath,
186-
usbDevices);
187-
}
188156
}

oshi-core-java25/src/main/java/oshi/hardware/platform/mac/MacUsbDeviceFFM.java

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
*/
55
package oshi.hardware.platform.mac;
66

7+
import static java.util.Collections.emptyList;
8+
79
import java.lang.foreign.MemorySegment;
810
import java.util.ArrayList;
9-
import java.util.Collections;
1011
import java.util.HashMap;
12+
import java.util.LinkedHashSet;
1113
import java.util.List;
1214
import java.util.Locale;
1315
import java.util.Map;
16+
import java.util.Set;
1417

1518
import org.slf4j.Logger;
1619
import org.slf4j.LoggerFactory;
1720

18-
import oshi.annotation.concurrent.Immutable;
1921
import oshi.ffm.mac.CoreFoundation.CFAllocatorRef;
2022
import oshi.ffm.mac.CoreFoundation.CFMutableDictionaryRef;
2123
import oshi.ffm.mac.CoreFoundation.CFStringRef;
@@ -24,26 +26,30 @@
2426
import oshi.ffm.mac.IOKit.IOIterator;
2527
import oshi.ffm.mac.IOKit.IORegistryEntry;
2628
import oshi.hardware.UsbDevice;
27-
import oshi.hardware.common.AbstractUsbDevice;
2829
import oshi.util.platform.mac.IOKitUtilFFM;
2930

3031
/**
31-
* Mac Usb Device FFM implementation.
32+
* Mac USB device helper using FFM/IOKit. Instantiates {@link MacUsbDevice} objects.
3233
*/
33-
@Immutable
34-
public class MacUsbDeviceFFM extends AbstractUsbDevice {
34+
public final class MacUsbDeviceFFM extends MacUsbDevice {
35+
36+
private MacUsbDeviceFFM() {
37+
}
3538

3639
private static final String IOUSB = "IOUSB";
3740
private static final String IOSERVICE = "IOService";
3841
private static final Logger LOG = LoggerFactory.getLogger(MacUsbDeviceFFM.class);
3942

40-
public MacUsbDeviceFFM(String name, String vendor, String vendorId, String productId, String serialNumber,
41-
String uniqueDeviceId, List<UsbDevice> connectedDevices) {
42-
super(name, vendor, vendorId, productId, serialNumber, uniqueDeviceId, connectedDevices);
43-
}
44-
43+
/**
44+
* Instantiates a list of {@link oshi.hardware.UsbDevice} objects, representing devices connected via a usb port
45+
* (including internal devices).
46+
*
47+
* @param tree If true, returns a list of controllers with their device tree. If false, returns a flat list of
48+
* devices excluding controllers.
49+
* @return a list of {@link oshi.hardware.UsbDevice} objects.
50+
*/
4551
public static List<UsbDevice> getUsbDevices(boolean tree) {
46-
List<UsbDevice> devices = getUsbDevices();
52+
List<UsbDevice> devices = queryUsbDevices();
4753
if (tree) {
4854
return devices;
4955
}
@@ -54,18 +60,18 @@ public static List<UsbDevice> getUsbDevices(boolean tree) {
5460
return deviceList;
5561
}
5662

57-
private static List<UsbDevice> getUsbDevices() {
63+
private static List<UsbDevice> queryUsbDevices() {
5864
Map<Long, String> nameMap = new HashMap<>();
5965
Map<Long, String> vendorMap = new HashMap<>();
6066
Map<Long, String> vendorIdMap = new HashMap<>();
6167
Map<Long, String> productIdMap = new HashMap<>();
6268
Map<Long, String> serialMap = new HashMap<>();
6369
Map<Long, List<Long>> hubMap = new HashMap<>();
6470

65-
List<Long> usbControllers = new ArrayList<>();
71+
Set<Long> usbControllers = new LinkedHashSet<>();
6672
IORegistryEntry root = IOKitUtilFFM.getRoot();
6773
if (root == null) {
68-
return Collections.emptyList();
74+
return emptyList();
6975
}
7076
IOIterator iter = root.getChildIterator(IOUSB);
7177
if (iter != null) {
@@ -88,6 +94,8 @@ private static List<UsbDevice> getUsbDevices() {
8894
}
8995
controller.release();
9096
}
97+
// If controller is null, id remains 0L, acting as an anonymous catch-all
98+
// controller so that devices whose parent cannot be identified are not lost.
9199
usbControllers.add(id);
92100
addDeviceAndChildrenToMaps(device, id, nameMap, vendorMap, vendorIdMap, productIdMap, serialMap,
93101
hubMap);
@@ -149,15 +157,6 @@ private static void addDeviceAndChildrenToMaps(IORegistryEntry device, long pare
149157
}
150158
}
151159

152-
private static void addDevicesToList(List<UsbDevice> deviceList, List<UsbDevice> list) {
153-
for (UsbDevice device : list) {
154-
deviceList.add(new MacUsbDeviceFFM(device.getName(), device.getVendor(), device.getVendorId(),
155-
device.getProductId(), device.getSerialNumber(), device.getUniqueDeviceId(),
156-
Collections.emptyList()));
157-
addDevicesToList(deviceList, device.getConnectedDevices());
158-
}
159-
}
160-
161160
private static void getControllerIdByLocation(long id, CFTypeRef locationId, CFStringRef locationIDKey,
162161
CFStringRef ioPropertyMatchKey, Map<Long, String> vendorIdMap, Map<Long, String> productIdMap) {
163162
// Build matching dict: { IOPropertyMatch: { locationID: <locationId> } }
@@ -203,21 +202,4 @@ private static void getControllerIdByLocation(long id, CFTypeRef locationId, CFS
203202
LOG.debug("Failed to retrieve controller vendor/product IDs for id {}", id, e);
204203
}
205204
}
206-
207-
private static MacUsbDeviceFFM getDeviceAndChildren(Long registryEntryId, String vid, String pid,
208-
Map<Long, String> nameMap, Map<Long, String> vendorMap, Map<Long, String> vendorIdMap,
209-
Map<Long, String> productIdMap, Map<Long, String> serialMap, Map<Long, List<Long>> hubMap) {
210-
String vendorId = vendorIdMap.getOrDefault(registryEntryId, vid);
211-
String productId = productIdMap.getOrDefault(registryEntryId, pid);
212-
List<Long> childIds = hubMap.getOrDefault(registryEntryId, new ArrayList<>());
213-
List<UsbDevice> usbDevices = new ArrayList<>();
214-
for (Long childId : childIds) {
215-
usbDevices.add(getDeviceAndChildren(childId, vendorId, productId, nameMap, vendorMap, vendorIdMap,
216-
productIdMap, serialMap, hubMap));
217-
}
218-
Collections.sort(usbDevices);
219-
return new MacUsbDeviceFFM(nameMap.getOrDefault(registryEntryId, vendorId + ":" + productId),
220-
vendorMap.getOrDefault(registryEntryId, ""), vendorId, productId,
221-
serialMap.getOrDefault(registryEntryId, ""), "0x" + Long.toHexString(registryEntryId), usbDevices);
222-
}
223205
}

oshi-core/src/main/java/oshi/SystemInfo.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
import oshi.hardware.platform.linux.LinuxHardwareAbstractionLayerJNA;
1313
import oshi.hardware.platform.mac.MacHardwareAbstractionLayerJNA;
1414
import oshi.hardware.platform.unix.aix.AixHardwareAbstractionLayer;
15-
import oshi.hardware.platform.windows.WindowsHardwareAbstractionLayer;
1615
import oshi.hardware.platform.unix.freebsd.FreeBsdHardwareAbstractionLayer;
1716
import oshi.hardware.platform.unix.openbsd.OpenBsdHardwareAbstractionLayer;
1817
import oshi.hardware.platform.unix.solaris.SolarisHardwareAbstractionLayer;
18+
import oshi.hardware.platform.windows.WindowsHardwareAbstractionLayer;
1919
import oshi.software.os.OperatingSystem;
2020
import oshi.software.os.linux.LinuxOperatingSystemJNA;
2121
import oshi.software.os.mac.MacOperatingSystemJNA;
2222
import oshi.software.os.unix.aix.AixOperatingSystem;
23-
import oshi.software.os.windows.WindowsOperatingSystem;
2423
import oshi.software.os.unix.freebsd.FreeBsdOperatingSystem;
2524
import oshi.software.os.unix.openbsd.OpenBsdOperatingSystem;
2625
import oshi.software.os.unix.solaris.SolarisOperatingSystem;
26+
import oshi.software.os.windows.WindowsOperatingSystem;
2727

2828
/**
2929
* System information. This is the main entry point to OSHI.

oshi-core/src/main/java/oshi/hardware/HardwareAbstractionLayer.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,33 @@ default List<LogicalVolumeGroup> getLogicalVolumeGroups() {
104104
* If the value of {@code tree} is true, the top level devices returned from this method are the USB Controllers;
105105
* connected hubs and devices in its device tree share that controller's bandwidth. If the value of {@code tree} is
106106
* false, USB devices (not controllers) are listed in a single flat list.
107+
* <p>
108+
* Note: in both cases each {@link UsbDevice} in the returned list may still report connected child devices via
109+
* {@link UsbDevice#getConnectedDevices()}. When {@code tree} is false the list is intended for simple iteration
110+
* over individual devices; callers should not recurse into {@link UsbDevice#getConnectedDevices()} or rely on
111+
* {@link Object#toString()} (which renders the full subtree) to avoid processing devices more than once.
112+
* <p>
113+
* To print the full device tree rooted at each controller:
114+
*
115+
* <pre>{@code
116+
* for (UsbDevice controller : hal.getUsbDevices(true)) {
117+
* System.out.println(controller); // toString() renders the full subtree
118+
* }
119+
* }</pre>
120+
*
121+
* To iterate individual devices without tree structure:
122+
*
123+
* <pre>{@code
124+
* for (UsbDevice device : hal.getUsbDevices(false)) {
125+
* // Use individual fields; avoid toString() as it includes the subtree
126+
* System.out.println(device.getName() + " [" + device.getVendorId() + ":" + device.getProductId() + "]");
127+
* }
128+
* }</pre>
107129
*
108-
* @param tree If {@code true}, returns devices connected to the existing device, accessible via
109-
* {@link UsbDevice#getConnectedDevices()}. If {@code false} returns devices as a flat list with no
110-
* connected device information.
130+
* @param tree If {@code true}, returns the USB Controllers as top-level entries, with connected hubs and devices
131+
* accessible via {@link UsbDevice#getConnectedDevices()}. If {@code false}, returns a flat list of
132+
* non-controller devices; {@link UsbDevice#getConnectedDevices()} may still be non-empty but should not
133+
* be iterated to avoid duplicates.
111134
* @return A list of UsbDevice objects representing (optionally) the USB Controllers and devices connected to them,
112135
* or an empty list if none are present
113136
*/

oshi-core/src/main/java/oshi/hardware/common/AbstractUsbDevice.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 The OSHI Project Contributors
2+
* Copyright 2016-2026 The OSHI Project Contributors
33
* SPDX-License-Identifier: MIT
44
*/
55
package oshi.hardware.common;
@@ -77,6 +77,19 @@ public int compareTo(UsbDevice usb) {
7777
return getName().compareTo(usb.getName());
7878
}
7979

80+
/**
81+
* Recursively adds USB devices from {@code list} to {@code deviceList}, depth-first.
82+
*
83+
* @param deviceList the target list to add devices to
84+
* @param list the source list of devices (and their children) to add
85+
*/
86+
protected static void addDevicesToList(List<UsbDevice> deviceList, List<UsbDevice> list) {
87+
for (UsbDevice device : list) {
88+
deviceList.add(device);
89+
addDevicesToList(deviceList, device.getConnectedDevices());
90+
}
91+
}
92+
8093
@Override
8194
public String toString() {
8295
return indentUsb(this, 1);

oshi-core/src/main/java/oshi/hardware/platform/linux/LinuxHardwareAbstractionLayerJNA.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public List<HWDiskStore> getDiskStores() {
3737

3838
@Override
3939
public List<UsbDevice> getUsbDevices(boolean tree) {
40-
return LinuxUsbDevice.getUsbDevices(tree);
40+
return LinuxUsbDeviceJNA.getUsbDevices(tree);
4141
}
4242

4343
@Override

0 commit comments

Comments
 (0)