From 00cee56904f685a1873545738d6a40b178aeb701 Mon Sep 17 00:00:00 2001 From: Ulli Hafner Date: Tue, 19 Nov 2024 23:07:29 +0100 Subject: [PATCH] Restructure package detectors. --- .../hafner/util/CSharpNamespaceDetector.java | 11 +- .../hm/hafner/util/JavaPackageDetector.java | 11 +- .../hm/hafner/util/KotlinPackageDetector.java | 10 +- ...kageDetector.java => PackageDetector.java} | 53 ++++-- .../hafner/util/PackageDetectorFactory.java | 31 +++ .../hm/hafner/util/PackageDetectorRunner.java | 37 ++++ .../edu/hm/hafner/util/PackageDetectors.java | 58 ------ .../util/PackageDetectorRunnerTest.java | 59 ++++++ .../hm/hafner/util/PackageDetectorsTest.java | 47 ----- .../edu/hm/hafner/util/ActionBinding.cs | 179 +----------------- .../hafner/util/{relative.txt => empty.txt} | 0 11 files changed, 168 insertions(+), 328 deletions(-) rename src/main/java/edu/hm/hafner/util/{AbstractPackageDetector.java => PackageDetector.java} (58%) create mode 100644 src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java create mode 100644 src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java delete mode 100644 src/main/java/edu/hm/hafner/util/PackageDetectors.java create mode 100644 src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java delete mode 100644 src/test/java/edu/hm/hafner/util/PackageDetectorsTest.java rename src/test/resources/edu/hm/hafner/util/{relative.txt => empty.txt} (100%) diff --git a/src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java b/src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java index 8ca03419..6b2eb2a6 100644 --- a/src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java +++ b/src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java @@ -2,22 +2,15 @@ import java.util.regex.Pattern; -import edu.hm.hafner.util.PackageDetectors.FileSystem; - /** * Detects the namespace of a C# workspace file. * * @author Ullrich Hafner */ -class CSharpNamespaceDetector extends AbstractPackageDetector { +class CSharpNamespaceDetector extends PackageDetector { private static final Pattern NAMESPACE_PATTERN = Pattern.compile("^\\s*namespace\\s+([^{]*)\\s*\\{?\\s*$"); - CSharpNamespaceDetector() { - this(new FileSystem()); - } - - @VisibleForTesting - CSharpNamespaceDetector(final FileSystem fileSystem) { + CSharpNamespaceDetector(final FileSystemFacade fileSystem) { super(fileSystem); } diff --git a/src/main/java/edu/hm/hafner/util/JavaPackageDetector.java b/src/main/java/edu/hm/hafner/util/JavaPackageDetector.java index 0b685b3e..66a09af2 100644 --- a/src/main/java/edu/hm/hafner/util/JavaPackageDetector.java +++ b/src/main/java/edu/hm/hafner/util/JavaPackageDetector.java @@ -2,23 +2,16 @@ import java.util.regex.Pattern; -import edu.hm.hafner.util.PackageDetectors.FileSystem; - /** * Detects the package name of a Java file. * * @author Ullrich Hafner */ -class JavaPackageDetector extends AbstractPackageDetector { +class JavaPackageDetector extends PackageDetector { private static final Pattern PACKAGE_PATTERN = Pattern.compile( "^\\s*package\\s*([a-z]+[.\\w]*)\\s*;.*"); - JavaPackageDetector() { - this(new FileSystem()); - } - - @VisibleForTesting - JavaPackageDetector(final FileSystem fileSystem) { + JavaPackageDetector(final FileSystemFacade fileSystem) { super(fileSystem); } diff --git a/src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java b/src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java index e62da7cd..45f4222c 100644 --- a/src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java +++ b/src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java @@ -2,23 +2,17 @@ import java.util.regex.Pattern; -import edu.hm.hafner.util.PackageDetectors.FileSystem; - /** * Detects the package name of a Kotlin file. * * @author Bastian Kersting */ -class KotlinPackageDetector extends AbstractPackageDetector { +class KotlinPackageDetector extends PackageDetector { private static final Pattern PACKAGE_PATTERN = Pattern.compile( "^\\s*package\\s*([a-z]+[.\\w]*)\\s*.*"); - KotlinPackageDetector() { - this(new FileSystem()); - } - @VisibleForTesting - KotlinPackageDetector(final FileSystem fileSystem) { + KotlinPackageDetector(final FileSystemFacade fileSystem) { super(fileSystem); } diff --git a/src/main/java/edu/hm/hafner/util/AbstractPackageDetector.java b/src/main/java/edu/hm/hafner/util/PackageDetector.java similarity index 58% rename from src/main/java/edu/hm/hafner/util/AbstractPackageDetector.java rename to src/main/java/edu/hm/hafner/util/PackageDetector.java index 76f7c7e8..af0cbd0c 100644 --- a/src/main/java/edu/hm/hafner/util/AbstractPackageDetector.java +++ b/src/main/java/edu/hm/hafner/util/PackageDetector.java @@ -5,32 +5,33 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.apache.commons.io.input.BOMInputStream; -import edu.hm.hafner.util.PackageDetectors.*; - -import static edu.hm.hafner.util.PackageDetectors.*; +import com.google.errorprone.annotations.MustBeClosed; /** * Base class for package detectors. * * @author Ullrich Hafner */ -abstract class AbstractPackageDetector { - private final FileSystem fileSystem; +abstract class PackageDetector { + private final FileSystemFacade fileSystem; /** - * Creates a new instance of {@link AbstractPackageDetector}. + * Creates a new instance of {@link PackageDetector}. * * @param fileSystem * file system facade */ - AbstractPackageDetector(final FileSystem fileSystem) { + PackageDetector(final FileSystemFacade fileSystem) { this.fileSystem = fileSystem; } @@ -44,21 +45,19 @@ abstract class AbstractPackageDetector { * * @return the detected package or namespace name */ - public String detectPackageName(final String fileName, final Charset charset) { - if (accepts(fileName)) { - try (InputStream stream = fileSystem.openFile(fileName)) { - return detectPackageName(stream, charset); - } - catch (IOException | InvalidPathException ignore) { - // ignore IO errors - } + public Optional detectPackageName(final String fileName, final Charset charset) { + try (var stream = fileSystem.openFile(fileName)) { + return detectPackageName(stream, charset); + } + catch (IOException | InvalidPathException ignore) { + // ignore IO errors } - return UNDEFINED_PACKAGE; + return Optional.empty(); } - @VisibleForTesting - String detectPackageName(final InputStream stream, final Charset charset) throws IOException { - try (BufferedReader buffer = new BufferedReader(new InputStreamReader(BOMInputStream.builder().setInputStream(stream).get(), charset))) { + private Optional detectPackageName(final InputStream stream, final Charset charset) throws IOException { + try (var buffer = new BufferedReader( + new InputStreamReader(BOMInputStream.builder().setInputStream(stream).get(), charset))) { return detectPackageName(buffer.lines()); } } @@ -72,17 +71,18 @@ String detectPackageName(final InputStream stream, final Charset charset) throws * * @return the detected package or namespace name */ - String detectPackageName(final Stream lines) { + private Optional detectPackageName(final Stream lines) { Pattern pattern = getPattern(); return lines.map(pattern::matcher) .filter(Matcher::matches) .findFirst() .map(matcher -> matcher.group(1)) - .orElse(UNDEFINED_PACKAGE).trim(); + .map(String::trim); } /** * Returns the Pattern for the Package Name in this kind of file. + * * @return the Pattern. */ abstract Pattern getPattern(); @@ -96,4 +96,15 @@ String detectPackageName(final Stream lines) { * @return {@code true} if the classifier accepts the specified file for processing. */ abstract boolean accepts(String fileName); + + /** + * Facade for file system operations. May be replaced by stubs in test cases. + */ + @VisibleForTesting + static class FileSystemFacade { + @MustBeClosed + InputStream openFile(final String fileName) throws IOException, InvalidPathException { + return Files.newInputStream(Paths.get(fileName)); + } + } } diff --git a/src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java b/src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java new file mode 100644 index 00000000..d5499543 --- /dev/null +++ b/src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java @@ -0,0 +1,31 @@ +package edu.hm.hafner.util; + +import edu.hm.hafner.util.PackageDetector.FileSystemFacade; + +/** + * Factory to create package detectors. + * + * @author Ullrich Hafner + */ +public final class PackageDetectorFactory { + /** + * Creates a new package detector runner that uses the detectors for Java, Kotlin, and C#. + * + * @return the package detector runner + */ + public static PackageDetectorRunner createPackageDetectors() { + return createPackageDetectors(new FileSystemFacade()); + } + + @VisibleForTesting + static PackageDetectorRunner createPackageDetectors(final FileSystemFacade facade) { + return new PackageDetectorRunner( + new JavaPackageDetector(facade), + new KotlinPackageDetector(facade), + new CSharpNamespaceDetector(facade)); + } + + private PackageDetectorFactory() { + // prevents instantiation + } +} diff --git a/src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java b/src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java new file mode 100644 index 00000000..bf3dc3d1 --- /dev/null +++ b/src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java @@ -0,0 +1,37 @@ +package edu.hm.hafner.util; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * Detects package or namespace names of files in the file system. + * + * @author Ullrich Hafner + */ +public class PackageDetectorRunner { + private final List detectors; + + PackageDetectorRunner(final PackageDetector... detectors) { + this.detectors = Arrays.asList(detectors); + } + + /** + * Detects the package name of the specified file based on several detector strategies. + * + * @param fileName + * the filename of the file to scan + * @param charset + * the charset to use when reading the source files + * + * @return the detected package name or {@link Optional#empty()} if no package name could be detected + */ + public Optional detectPackageName(final String fileName, final Charset charset) { + return detectors.stream() + .filter(detector -> detector.accepts(fileName)) + .map(detector -> detector.detectPackageName(fileName, charset)) + .flatMap(Optional::stream) + .findFirst(); + } +} diff --git a/src/main/java/edu/hm/hafner/util/PackageDetectors.java b/src/main/java/edu/hm/hafner/util/PackageDetectors.java deleted file mode 100644 index 8cd6a0e4..00000000 --- a/src/main/java/edu/hm/hafner/util/PackageDetectors.java +++ /dev/null @@ -1,58 +0,0 @@ -package edu.hm.hafner.util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Paths; -import java.util.List; - -import com.google.errorprone.annotations.MustBeClosed; - -/** - * Provides convenient methods to determine the package or namespace names of a file. - * - * @author Ullrich Hafner - */ -class PackageDetectors { - /** If no package could be assigned this value is used as package name. */ - static final String UNDEFINED_PACKAGE = "-"; - - private final List detectors; - - @VisibleForTesting - PackageDetectors(final List detectors) { - this.detectors = detectors; - } - - /** - * Detects the package name of the specified file based on several detector strategies. - * - * @param fileName - * the filename of the file to scan - * @param charset - * the charset to use when reading the source files - * - * @return the package name or the String {@link #UNDEFINED_PACKAGE} if no package could be detected - */ - public String detectPackageName(final String fileName, final Charset charset) { - for (AbstractPackageDetector detector : detectors) { - if (detector.accepts(fileName)) { - return detector.detectPackageName(fileName, charset); - } - } - return UNDEFINED_PACKAGE; - } - - /** - * Facade for file system operations. May be replaced by stubs in test cases. - */ - @VisibleForTesting - static class FileSystem { - @MustBeClosed - InputStream openFile(final String fileName) throws IOException, InvalidPathException { - return Files.newInputStream(Paths.get(fileName)); - } - } -} diff --git a/src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java b/src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java new file mode 100644 index 00000000..a25dbca7 --- /dev/null +++ b/src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java @@ -0,0 +1,59 @@ +package edu.hm.hafner.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import edu.hm.hafner.util.PackageDetector.FileSystemFacade; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests the class {@link PackageDetectorRunner}. + * + * @author Ullrich Hafner + */ +class PackageDetectorRunnerTest extends ResourceTest { + @ParameterizedTest(name = "{index} => file={0}, expected package={1}") + @CsvSource({ + "MavenJavaTest.txt.java, hudson.plugins.tasks.util", + "ActionBinding.cs, Avaloq.SmartClient.Utilities", + "KotlinTest.txt.kt, edu.hm.kersting", + }) + void shouldExtractPackageNames(final String fileName, final String expectedPackage) throws IOException { + assertThat(detect(fileName)).contains(expectedPackage); + } + + @ParameterizedTest(name = "{index} => file={0}, no package found") + @ValueSource(strings = {"MavenJavaTest.txt", "empty.txt", "KotlinTest.txt"}) + void shouldNotAcceptFile(final String fileName) throws IOException { + assertThat(detect(fileName)).isEmpty(); + } + + private Optional detect(final String fileName) throws IOException { + try (InputStream stream = asInputStream(fileName)) { + var fileSystem = mock(FileSystemFacade.class); + when(fileSystem.openFile(fileName)).thenReturn(stream); + + var detectors = PackageDetectorFactory.createPackageDetectors(fileSystem); + + return detectors.detectPackageName(fileName, StandardCharsets.UTF_8); + } + } + + @Test + void shouldHandleException() throws IOException { + var fileSystem = mock(FileSystemFacade.class); + when(fileSystem.openFile(anyString())).thenThrow(new IOException("Simulated")); + + assertThat(PackageDetectorFactory.createPackageDetectors(fileSystem) + .detectPackageName("file.java", StandardCharsets.UTF_8)).isEmpty(); + } +} diff --git a/src/test/java/edu/hm/hafner/util/PackageDetectorsTest.java b/src/test/java/edu/hm/hafner/util/PackageDetectorsTest.java deleted file mode 100644 index cfbd92c5..00000000 --- a/src/test/java/edu/hm/hafner/util/PackageDetectorsTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package edu.hm.hafner.util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import edu.hm.hafner.util.PackageDetectors.FileSystem; - -import static edu.hm.hafner.util.assertions.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests the class {@link PackageDetectors}. - * - * @author Ullrich Hafner - */ -class PackageDetectorsTest extends ResourceTest { - @ParameterizedTest(name = "{index} => file={0}, expected package={1}") - @CsvSource({ - "MavenJavaTest.txt.java, hudson.plugins.tasks.util", - "ActionBinding.cs, Avaloq.SmartClient.Utilities", - "MavenJavaTest.txt, -", - "relative.txt, -", - "KotlinTest.txt, -", - "KotlinTest.txt.kt, edu.hm.kersting", - }) - void shouldExtractPackageNames(final String fileName, final String expectedPackage) throws IOException { - try (InputStream stream = asInputStream(fileName)) { - FileSystem fileSystem = mock(FileSystem.class); - when(fileSystem.openFile(fileName)).thenReturn(stream); - - ArrayList detectors = new ArrayList<>(Arrays.asList( - new JavaPackageDetector(fileSystem), - new CSharpNamespaceDetector(fileSystem), - new KotlinPackageDetector(fileSystem) - )); - - assertThat(new PackageDetectors(detectors).detectPackageName(fileName, StandardCharsets.UTF_8)) - .isEqualTo(expectedPackage); - } - } -} diff --git a/src/test/resources/edu/hm/hafner/util/ActionBinding.cs b/src/test/resources/edu/hm/hafner/util/ActionBinding.cs index e930faf8..05878789 100644 --- a/src/test/resources/edu/hm/hafner/util/ActionBinding.cs +++ b/src/test/resources/edu/hm/hafner/util/ActionBinding.cs @@ -13,179 +13,6 @@ namespace Avaloq.SmartClient.Utilities { /// /// Acts as mediator between an and a clickable Windows Forms control. /// - /// - /// An brings the Java Action concept to any clickable Windows Forms control. An - /// connects the following properties and events: - /// - /// of a control to of an action - /// of a control to of an action - /// of a control to of an action - /// of a control to of an action - /// - /// Whenever an action property changes, the corresponding control property is adjusted accordingly. If the control is clicked then - /// the method is invoked. Don't forget to call on this object in order to cleanup the event - /// listeners. - /// - /// - /// - /// class HelloWorldAction : Action { - /// public HelloWorldAction() - /// : base("Hello") { - /// } - /// - /// public override void Run() { - /// Debug.WriteLine("Hello World"); - /// } - /// } - /// - /// [...] - /// - /// Action action = new HelloWorldAction(); - /// Button button = new Button(); - /// ActionBinding binding = ActionBinding.Bind(button, action); - /// - /// [...] - /// FIXME: here is a fixme - /// binding.Dispose(); - /// - /// - public class ActionBinding : IDisposable { - /// - /// The logger to trace debug messages. - /// - static readonly ILog logger = LogManager.GetLogger(typeof(ActionBinding)); - - /// - /// The action to connect to the control. - /// - readonly Action action; - - /// - /// The control to connect to the action. - /// - readonly Control control; - - /// - /// Binds the specified action to a control. - /// - /// the control to connect to the action - /// the action to connect to the control - [SuppressMessage("Microsoft.Usage", "CA1806")] - [SuppressMessage("Avaloq.Design", "ParameterContractRule")] - public static ActionBinding Bind(Action action, Control control) { - return new ActionBinding(action, control); - } - - /// - /// Creates a new control adapter. - /// - /// the control to connect to the action - /// the action to connect to the control - public ActionBinding(Action action, Control control) { - #region Preconditions - - Contract.RequireNotNullArgument(control, "control"); - Contract.RequireNotNullArgument(action, "action"); - - #endregion - - this.action = action; - this.action.PropertyChanged += new PropertyChangedEventHandler(UpdateControlProperties); - - this.control = control; - this.control.Click += new EventHandler(RunAction); - - UpdateControlProperties(); - } - - /// - /// Handles a property changed event raised by the action. All - /// control properties are updated. - /// - /// not used - /// not used - void UpdateControlProperties(object sender, PropertyChangedEventArgs e) { - #region Preconditions - - Contract.RequireNotNullArgument(e, "e"); - - #endregion - - if (logger.IsDebugEnabled) { - logger.Debug(string.Format("Property '{0}' of action '{1}' changed from '{2}' to '{3}'.", - e.PropertyName, action.Name, ChangedPropertyEventArgs.GetOldValue(e), ChangedPropertyEventArgs.GetNewValue(e))); - } - UpdateControlProperties(); - } - - /// - /// Updates all control properties with the values provided by the corresponding action properties. - /// - void UpdateControlProperties() { - if (!alreadyDisposed) { - control.Enabled = action.Runnable; - control.Text = action.Text; - control.Name = action.Name; - } - } - - /// - /// Handles a event raised by the control. - /// Executes the action method . - /// - /// not used - /// not used - void RunAction(object sender, EventArgs e) { - if (logger.IsDebugEnabled) { - logger.Debug(string.Format("Control '{0}' has been clicked. Running corresponding action.", action.Name)); - } - action.Run(); - } - - #region Dispose Pattern - - /// - /// Indicates wether this instance already has been disposed. - /// - bool alreadyDisposed; - - /// - /// Releases unmanaged resources and performs other cleanup operations before this - /// instance is reclaimed by garbage collection. - /// - ~ActionBinding() { - Dispose(false); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources of - /// this instance. - /// - /// if set to true then we are called by , - /// otherwise by the destructor - /// - /// If your derived class needs to perform additional cleanup, override this method and call it after the derived class cleanup. - /// - protected virtual void Dispose(bool isDisposing) { - if (alreadyDisposed) { - return; - } - if (isDisposing) { - action.PropertyChanged -= new PropertyChangedEventHandler(UpdateControlProperties); - control.Click -= new EventHandler(RunAction); - } - - alreadyDisposed = true; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(true); - } - - #endregion - } -} \ No newline at end of file + public class ActionBinding : IDisposable + // Rest of the file is omitted +} diff --git a/src/test/resources/edu/hm/hafner/util/relative.txt b/src/test/resources/edu/hm/hafner/util/empty.txt similarity index 100% rename from src/test/resources/edu/hm/hafner/util/relative.txt rename to src/test/resources/edu/hm/hafner/util/empty.txt