Skip to content

Commit

Permalink
Import package detectors from analysis-model.
Browse files Browse the repository at this point in the history
Refactor the detectors to use an optional as return value.
  • Loading branch information
uhafner committed Nov 20, 2024
1 parent 37da6da commit 9e10ab7
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the namespace of a C# workspace file.
*
* @author Ullrich Hafner
*/
class CSharpNamespaceDetector extends PackageDetector {
private static final Pattern NAMESPACE_PATTERN = Pattern.compile("^\\s*namespace\\s+([^{]*)\\s*\\{?\\s*$");

CSharpNamespaceDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
public boolean accepts(final String fileName) {
return fileName.endsWith(".cs");
}

@Override
Pattern getPattern() {
return NAMESPACE_PATTERN;
}
}

27 changes: 27 additions & 0 deletions src/main/java/edu/hm/hafner/util/JavaPackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the package name of a Java file.
*
* @author Ullrich Hafner
*/
class JavaPackageDetector extends PackageDetector {
private static final Pattern PACKAGE_PATTERN = Pattern.compile(
"^\\s*package\\s*([a-z]+[.\\w]*)\\s*;.*");

JavaPackageDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
Pattern getPattern() {
return PACKAGE_PATTERN;
}

@Override
public boolean accepts(final String fileName) {
return fileName.endsWith(".java");
}
}
28 changes: 28 additions & 0 deletions src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the package name of a Kotlin file.
*
* @author Bastian Kersting
*/
class KotlinPackageDetector extends PackageDetector {
private static final Pattern PACKAGE_PATTERN = Pattern.compile(
"^\\s*package\\s*([a-z]+[.\\w]*)\\s*.*");

@VisibleForTesting
KotlinPackageDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
Pattern getPattern() {
return PACKAGE_PATTERN;
}

@Override
boolean accepts(final String fileName) {
return fileName.endsWith(".kt");
}
}
110 changes: 110 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package edu.hm.hafner.util;

import java.io.BufferedReader;
import java.io.IOException;
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 com.google.errorprone.annotations.MustBeClosed;

/**
* Base class for package detectors.
*
* @author Ullrich Hafner
*/
abstract class PackageDetector {
private final FileSystemFacade fileSystem;

/**
* Creates a new instance of {@link PackageDetector}.
*
* @param fileSystem
* file system facade
*/
PackageDetector(final FileSystemFacade fileSystem) {
this.fileSystem = fileSystem;
}

/**
* Detects the package or namespace name of the specified file.
*
* @param fileName
* the file name of the file to scan
* @param charset
* the charset to use when reading the source files
*
* @return the detected package or namespace name
*/
public Optional<String> 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 Optional.empty();
}

private Optional<String> 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());
}
}

/**
* Detects the package or namespace name of the specified input stream. The stream will be closed automatically by
* the caller of this method.
*
* @param lines
* the content of the file to scan
*
* @return the detected package or namespace name
*/
private Optional<String> detectPackageName(final Stream<String> lines) {
Pattern pattern = getPattern();
return lines.map(pattern::matcher)
.filter(Matcher::matches)
.findFirst()
.map(matcher -> matcher.group(1))
.map(String::trim);
}

/**
* Returns the Pattern for the Package Name in this kind of file.
*
* @return the Pattern.
*/
abstract Pattern getPattern();

/**
* Returns whether this classifier accepts the specified file for processing.
*
* @param fileName
* the file name
*
* @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));
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java
Original file line number Diff line number Diff line change
@@ -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
}
}
37 changes: 37 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java
Original file line number Diff line number Diff line change
@@ -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<PackageDetector> 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<String> 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();
}
}
59 changes: 59 additions & 0 deletions src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
18 changes: 18 additions & 0 deletions src/test/resources/edu/hm/hafner/util/ActionBinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Author: Hafner Ullrich
*/

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using Avaloq.Utilities;
using log4net;

namespace Avaloq.SmartClient.Utilities {
/// <summary>
/// Acts as mediator between an <see cref="Action"/> and a clickable Windows Forms control.
/// </summary>
public class ActionBinding : IDisposable
// Rest of the file is omitted
}
5 changes: 5 additions & 0 deletions src/test/resources/edu/hm/hafner/util/KotlinTest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package edu.hm.kersting

class HelloWorld {
fun main() = println("Hello World")
}
5 changes: 5 additions & 0 deletions src/test/resources/edu/hm/hafner/util/KotlinTest.txt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package edu.hm.kersting

class HelloWorld {
fun main() = println("Hello World")
}
6 changes: 6 additions & 0 deletions src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hudson.plugins.tasks.util;

/**
* Indicates an orderly abortion of the processing.
*
// Rest of the file is omitted
6 changes: 6 additions & 0 deletions src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hudson.plugins.tasks.util;

/**
* Indicates an orderly abortion of the processing.
*/
// Rest of the file is omitted

0 comments on commit 9e10ab7

Please sign in to comment.