From 2e81dbf228014345e95af2200368e78c6fbf7ebf Mon Sep 17 00:00:00 2001 From: Philippe Charles Date: Mon, 25 May 2020 09:28:00 +0200 Subject: [PATCH] Add fallback WinRegistry --- .../impl/InternalCopyOfRegWrapper.java | 107 ++++++++++++++++++ .../impl/InternalCopyofProcessReader.java | 80 +++++++++++++ .../ec/util/desktop/impl/JnaRegistry.java | 8 ++ .../ec/util/desktop/impl/RegRegistry.java | 70 ++++++++++++ .../ec/util/desktop/impl/WinRegistry.java | 4 +- .../ec/util/desktop/impl/JnaRegistryTest.java | 50 ++++++++ .../ec/util/desktop/impl/RegRegistryTest.java | 42 +++++++ .../ec/util/desktop/impl/WinRegistryTest.java | 95 ++++++++++++++++ 8 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyOfRegWrapper.java create mode 100644 java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyofProcessReader.java create mode 100644 java-desktop-util-os/src/main/java/ec/util/desktop/impl/RegRegistry.java create mode 100644 java-desktop-util-os/src/test/java/ec/util/desktop/impl/JnaRegistryTest.java create mode 100644 java-desktop-util-os/src/test/java/ec/util/desktop/impl/RegRegistryTest.java create mode 100644 java-desktop-util-os/src/test/java/ec/util/desktop/impl/WinRegistryTest.java diff --git a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyOfRegWrapper.java b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyOfRegWrapper.java new file mode 100644 index 0000000..d4dd5c3 --- /dev/null +++ b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyOfRegWrapper.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * + * @author Philippe Charles + */ +@lombok.experimental.UtilityClass +class InternalCopyOfRegWrapper { + + public static final String COMMAND = "reg"; + + @NonNull + public Map> query(@NonNull String keyName, boolean recursive) throws IOException { + Objects.requireNonNull(keyName); + try (BufferedReader reader = InternalCopyofProcessReader.newReader(getArgs(keyName, recursive))) { + return parse(reader); + } + } + + String[] getArgs(String keyName, boolean recursive) { + List args = new ArrayList<>(); + args.add(COMMAND); + args.add("query"); + args.add(keyName); + if (recursive) { + args.add("/s"); + } + return args.toArray(new String[0]); + } + + Map> parse(BufferedReader reader) throws IOException { + Map> result = new LinkedHashMap<>(); + String line; + String subKey = null; + List values = null; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) { + if (subKey == null) { + subKey = line; + values = new ArrayList<>(); + } else { + RegValue regValue = RegValue.parseOrNull(line); + if (regValue != null) { + values.add(regValue); + } else { + result.put(subKey, values); + subKey = line; + values = new ArrayList<>(); + } + } + } + } + if (subKey != null) { + result.put(subKey, values); + } + return result; + } + + @lombok.Value + public static final class RegValue { + + private static final Pattern PATTERN = Pattern.compile("^[ ]{4}(.+)[ ]{4}(REG_(?:SZ|MULTI_SZ|EXPAND_SZ|DWORD|QWORD|BINARY|NONE))[ ]{4}(.*)$"); + + @Nullable + public static RegValue parseOrNull(@NonNull CharSequence line) { + Matcher m = PATTERN.matcher(line); + return m.matches() ? new RegValue(m.group(1), m.group(2), m.group(3)) : null; + } + + @lombok.NonNull + private String name; + + @lombok.NonNull + private String dataType; + + @lombok.NonNull + private String value; + } +} diff --git a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyofProcessReader.java b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyofProcessReader.java new file mode 100644 index 0000000..f2defcf --- /dev/null +++ b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/InternalCopyofProcessReader.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * + * @author Philippe Charles + */ +@lombok.experimental.UtilityClass +class InternalCopyofProcessReader { + + @NonNull + public static BufferedReader newReader(@NonNull String... args) throws IOException { + return newReader(new ProcessBuilder(args).start()); + } + + @NonNull + public static BufferedReader newReader(@NonNull Process process) throws IOException { + return new BufferedReader(new InputStreamReader(new ProcessInputStream(process), Charset.defaultCharset())); + } + + private static final class ProcessInputStream extends InputStream { + + @lombok.experimental.Delegate(excludes = Closeable.class) + private final InputStream delegate; + + private final Process process; + + public ProcessInputStream(Process process) { + this.delegate = process.getInputStream(); + this.process = process; + } + + @Override + public void close() throws IOException { + try { + readUntilEnd(); + waitForEndOfProcess(); + } finally { + delegate.close(); + } + } + + // we need the process to end, else we'll get an illegal Thread State Exception + private void readUntilEnd() throws IOException { + while (delegate.read() != -1) { + } + } + + private void waitForEndOfProcess() throws IOException { + try { + process.waitFor(); + } catch (InterruptedException ex) { + throw new IOException(ex); + } + } + } +} diff --git a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/JnaRegistry.java b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/JnaRegistry.java index e8a5aa8..ff196c5 100644 --- a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/JnaRegistry.java +++ b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/JnaRegistry.java @@ -20,6 +20,7 @@ import com.sun.jna.platform.win32.Win32Exception; import com.sun.jna.platform.win32.WinReg; import java.io.IOException; +import java.util.Objects; import java.util.SortedMap; /** @@ -30,6 +31,8 @@ final class JnaRegistry extends WinRegistry { @Override public boolean keyExists(Root root, String key) throws IOException { + Objects.requireNonNull(root); + Objects.requireNonNull(key); try { return Advapi32Util.registryKeyExists(convert(root), key); } catch (Win32Exception | UnsatisfiedLinkError ex) { @@ -39,6 +42,9 @@ public boolean keyExists(Root root, String key) throws IOException { @Override public Object getValue(Root root, String key, String name) throws IOException { + Objects.requireNonNull(root); + Objects.requireNonNull(key); + Objects.requireNonNull(name); try { WinReg.HKEY hkey = convert(root); return Advapi32Util.registryValueExists(hkey, key, name) ? Advapi32Util.registryGetValue(hkey, key, name) : null; @@ -49,6 +55,8 @@ public Object getValue(Root root, String key, String name) throws IOException { @Override public SortedMap getValues(Root root, String key) throws IOException { + Objects.requireNonNull(root); + Objects.requireNonNull(key); try { WinReg.HKEY hkey = convert(root); return Advapi32Util.registryKeyExists(hkey, key) ? Advapi32Util.registryGetValues(hkey, key) : Util.EMPTY_SORTED_MAP; diff --git a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/RegRegistry.java b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/RegRegistry.java new file mode 100644 index 0000000..19ea2cb --- /dev/null +++ b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/RegRegistry.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import ec.util.desktop.impl.InternalCopyOfRegWrapper.RegValue; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * + * @author Philippe Charles + */ +final class RegRegistry extends WinRegistry { + + private static final String KEY_SEPARATOR = "\\"; + + private List getValuesOrNull(WinRegistry.Root root, String key) throws IOException { + Objects.requireNonNull(root); + Objects.requireNonNull(key); + String keyName = root.name() + KEY_SEPARATOR + key; + return InternalCopyOfRegWrapper.query(keyName, false).get(keyName); + } + + @Override + public boolean keyExists(WinRegistry.Root root, String key) throws IOException { + List data = getValuesOrNull(root, key); + return data != null; + } + + @Override + public Object getValue(WinRegistry.Root root, String key, String name) throws IOException { + List data = getValuesOrNull(root, key); + Objects.requireNonNull(name); + return data != null + ? data + .stream() + .filter(regValue -> regValue.getName().equals(name)) + .map(regValue -> regValue.getValue()) + .findFirst() + .orElse(null) + : null; + } + + @Override + public SortedMap getValues(WinRegistry.Root root, String key) throws IOException { + List data = getValuesOrNull(root, key); + return data != null + ? data.stream().collect(Collectors.toMap(RegValue::getName, regValue -> (Object) regValue.getValue(), (l, r) -> l, TreeMap::new)) + : Collections.emptySortedMap(); + } +} diff --git a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/WinRegistry.java b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/WinRegistry.java index b80dbae..74e923d 100644 --- a/java-desktop-util-os/src/main/java/ec/util/desktop/impl/WinRegistry.java +++ b/java-desktop-util-os/src/main/java/ec/util/desktop/impl/WinRegistry.java @@ -72,8 +72,8 @@ private static WinRegistry createInstance() { return new JnaRegistry(); } // fallback - log.log(Level.INFO, "Using NoOpRegistry"); - return noOp(); + log.log(Level.INFO, "Using RegRegistry"); + return new RegRegistry(); } } diff --git a/java-desktop-util-os/src/test/java/ec/util/desktop/impl/JnaRegistryTest.java b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/JnaRegistryTest.java new file mode 100644 index 0000000..0d71e12 --- /dev/null +++ b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/JnaRegistryTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import java.io.IOException; +import org.junit.Assume; +import org.junit.Test; + +/** + * + * @author Philippe Charles + */ +public class JnaRegistryTest { + + static boolean isJnaAvailable() { + return Util.isClassAvailable("com.sun.jna.platform.win32.Advapi32Util"); + } + + @Test + public void testKeyExists() throws IOException { + Assume.assumeTrue(isJnaAvailable()); + WinRegistryTest.testKeyExists(new JnaRegistry()); + } + + @Test + public void testGetValue() throws IOException { + Assume.assumeTrue(isJnaAvailable()); + WinRegistryTest.testGetValue(new JnaRegistry()); + } + + @Test + public void testGetValues() throws IOException { + Assume.assumeTrue(isJnaAvailable()); + WinRegistryTest.testGetValues(new JnaRegistry()); + } +} diff --git a/java-desktop-util-os/src/test/java/ec/util/desktop/impl/RegRegistryTest.java b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/RegRegistryTest.java new file mode 100644 index 0000000..f29409b --- /dev/null +++ b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/RegRegistryTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import java.io.IOException; +import org.junit.Test; + +/** + * + * @author Philippe Charles + */ +public class RegRegistryTest { + + @Test + public void testKeyExists() throws IOException { + WinRegistryTest.testKeyExists(new RegRegistry()); + } + + @Test + public void testGetValue() throws IOException { + WinRegistryTest.testGetValue(new RegRegistry()); + } + + @Test + public void testGetValues() throws IOException { + WinRegistryTest.testGetValues(new RegRegistry()); + } +} diff --git a/java-desktop-util-os/src/test/java/ec/util/desktop/impl/WinRegistryTest.java b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/WinRegistryTest.java new file mode 100644 index 0000000..00233b2 --- /dev/null +++ b/java-desktop-util-os/src/test/java/ec/util/desktop/impl/WinRegistryTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 National Bank of Belgium + * + * Licensed under the EUPL, Version 1.1 or - as soon they will be approved + * by the European Commission - subsequent versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * http://ec.europa.eu/idabc/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + */ +package ec.util.desktop.impl; + +import static ec.util.desktop.impl.WinRegistry.Root.HKEY_LOCAL_MACHINE; +import java.io.IOException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import org.junit.Assume; + +/** + * + * @author Philippe Charles + */ +public class WinRegistryTest { + + static boolean isWindows() { + return System.getProperty("os.name").contains("Windows"); + } + + private static final String CURRENT_VERSION_NODE = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + private static final String SYSTEM_ROOT_LEAF = "SystemRoot"; + private static final String MISSING_NODE = "XYZ"; + + static void testKeyExists(WinRegistry reg) throws IOException { + Assume.assumeTrue(isWindows()); + + assertThatNullPointerException() + .isThrownBy(() -> reg.keyExists(null, CURRENT_VERSION_NODE)); + + assertThatNullPointerException() + .isThrownBy(() -> reg.keyExists(HKEY_LOCAL_MACHINE, null)); + + assertThat(reg.keyExists(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE)) + .isTrue(); + + assertThat(reg.keyExists(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE + "\\" + SYSTEM_ROOT_LEAF)) + .isFalse(); + + assertThat(reg.keyExists(HKEY_LOCAL_MACHINE, MISSING_NODE)) + .isFalse(); + } + + static void testGetValue(WinRegistry reg) throws IOException { + Assume.assumeTrue(isWindows()); + + assertThatNullPointerException() + .isThrownBy(() -> reg.getValue(null, CURRENT_VERSION_NODE, SYSTEM_ROOT_LEAF)); + + assertThatNullPointerException() + .isThrownBy(() -> reg.getValue(HKEY_LOCAL_MACHINE, null, SYSTEM_ROOT_LEAF)); + + assertThatNullPointerException() + .isThrownBy(() -> reg.getValue(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE, null)); + + assertThat(reg.getValue(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE, SYSTEM_ROOT_LEAF)) + .isEqualTo(System.getenv("SYSTEMROOT")); + + assertThat(reg.getValue(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE, "xyz")) + .isNull(); + + assertThat(reg.getValue(HKEY_LOCAL_MACHINE, MISSING_NODE, SYSTEM_ROOT_LEAF)) + .isNull(); + } + + static void testGetValues(WinRegistry reg) throws IOException { + Assume.assumeTrue(isWindows()); + + assertThatNullPointerException() + .isThrownBy(() -> reg.getValues(null, CURRENT_VERSION_NODE)); + + assertThatNullPointerException() + .isThrownBy(() -> reg.getValues(HKEY_LOCAL_MACHINE, null)); + + assertThat(reg.getValues(HKEY_LOCAL_MACHINE, CURRENT_VERSION_NODE)) + .containsEntry(SYSTEM_ROOT_LEAF, System.getenv("SYSTEMROOT")); + + assertThat(reg.getValues(HKEY_LOCAL_MACHINE, MISSING_NODE)) + .isEmpty(); + } +}