diff --git a/src/main/java/reposense/authorship/AuthorshipReporter.java b/src/main/java/reposense/authorship/AuthorshipReporter.java index 24f55ce8f4..ba27a1e89a 100644 --- a/src/main/java/reposense/authorship/AuthorshipReporter.java +++ b/src/main/java/reposense/authorship/AuthorshipReporter.java @@ -1,7 +1,6 @@ package reposense.authorship; import java.util.List; -import java.util.Objects; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -10,6 +9,7 @@ import reposense.authorship.model.FileResult; import reposense.model.RepoConfiguration; import reposense.system.LogsManager; +import reposense.util.function.Failable; /** @@ -47,14 +47,16 @@ public AuthorshipSummary generateAuthorshipSummary(RepoConfiguration config) { List fileResults = textFileInfos.stream() .map(fileInfo -> fileInfoAnalyzer.analyzeTextFile(config, fileInfo)) - .filter(Objects::nonNull) + .filter(Failable::isPresent) + .map(Failable::get) .collect(Collectors.toList()); List binaryFileInfos = fileInfoExtractor.extractBinaryFileInfos(config); List binaryFileResults = binaryFileInfos.stream() .map(fileInfo -> fileInfoAnalyzer.analyzeBinaryFile(config, fileInfo)) - .filter(Objects::nonNull) + .filter(Failable::isPresent) + .map(Failable::get) .collect(Collectors.toList()); fileResults.addAll(binaryFileResults); diff --git a/src/main/java/reposense/authorship/FileInfoAnalyzer.java b/src/main/java/reposense/authorship/FileInfoAnalyzer.java index 5efd8ce7c3..20bb4ae531 100644 --- a/src/main/java/reposense/authorship/FileInfoAnalyzer.java +++ b/src/main/java/reposense/authorship/FileInfoAnalyzer.java @@ -7,7 +7,6 @@ import java.time.LocalDateTime; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.logging.Logger; @@ -23,6 +22,7 @@ import reposense.system.LogsManager; import reposense.util.FileUtil; import reposense.util.StringsUtil; +import reposense.util.function.Failable; /** * Analyzes the target and information given in the {@link FileInfo}. @@ -45,50 +45,41 @@ public class FileInfoAnalyzer { /** * Analyzes the lines of the file, given in the {@code fileInfo}, that has changed in the time period provided * by {@code config}. - * Returns null if the file is missing from the local system, or none of the - * {@link Author} specified in {@code config} contributed to the file in {@code fileInfo}. + * Returns empty {@code Failable} if the file is missing from the local system, + * or none of the {@link Author} specified in {@code config} contributed to the file in {@code fileInfo}. */ - public FileResult analyzeTextFile(RepoConfiguration config, FileInfo fileInfo) { + public Failable analyzeTextFile(RepoConfiguration config, FileInfo fileInfo) { String relativePath = fileInfo.getPath(); - if (Files.notExists(Paths.get(config.getRepoRoot(), relativePath))) { - logger.severe(String.format(MESSAGE_FILE_MISSING, relativePath)); - return null; - } - - if (FileUtil.isEmptyFile(config.getRepoRoot(), relativePath)) { - return null; - } - - aggregateBlameAuthorModifiedAndDateInfo(config, fileInfo); - fileInfo.setFileType(config.getFileType(fileInfo.getPath())); - - AnnotatorAnalyzer.aggregateAnnotationAuthorInfo(fileInfo, config.getAuthorConfig()); - - if (!config.getAuthorList().isEmpty() && fileInfo.isAllAuthorsIgnored(config.getAuthorList())) { - return null; - } - - return generateTextFileResult(fileInfo); + // note that the predicates in filter() test for the negation of the previous failure conditions + return Failable.ofNullable(() -> relativePath) + .filter(x -> Files.exists(Paths.get(config.getRepoRoot(), x))) + .ifAbsent(() -> logger.severe(String.format(MESSAGE_FILE_MISSING, relativePath))) + .filter(x -> !FileUtil.isEmptyFile(config.getRepoRoot(), x)) + .ifPresent(x -> { + aggregateBlameAuthorModifiedAndDateInfo(config, fileInfo); + fileInfo.setFileType(config.getFileType(fileInfo.getPath())); + + AnnotatorAnalyzer.aggregateAnnotationAuthorInfo(fileInfo, config.getAuthorConfig()); + }) + .filter(x -> config.getAuthorList().isEmpty() || !fileInfo.isAllAuthorsIgnored(config.getAuthorList())) + .map(x -> generateTextFileResult(fileInfo)); } /** * Analyzes the binary file, given in the {@code fileInfo}, that has changed in the time period provided * by {@code config}. - * Returns null if the file is missing from the local system, or none of the - * {@link Author} specified in {@code config} contributed to the file in {@code fileInfo}. + * Returns empty {@code Failable} if the file is missing from the local system, + * or none of the {@link Author} specified in {@code config} contributed to the file in {@code fileInfo}. */ - public FileResult analyzeBinaryFile(RepoConfiguration config, FileInfo fileInfo) { + public Failable analyzeBinaryFile(RepoConfiguration config, FileInfo fileInfo) { String relativePath = fileInfo.getPath(); - if (Files.notExists(Paths.get(config.getRepoRoot(), relativePath))) { - logger.severe(String.format(MESSAGE_FILE_MISSING, relativePath)); - return null; - } - - fileInfo.setFileType(config.getFileType(fileInfo.getPath())); - - return generateBinaryFileResult(config, fileInfo); + return Failable.ofNullable(() -> relativePath) + .filter(x -> Files.exists(Paths.get(config.getRepoRoot(), relativePath))) + .ifAbsent(() -> logger.severe(String.format(MESSAGE_FILE_MISSING, relativePath))) + .ifPresent(x -> fileInfo.setFileType(config.getFileType(fileInfo.getPath()))) + .flatMap(x -> generateBinaryFileResult(config, fileInfo)); } /** @@ -109,29 +100,29 @@ private FileResult generateTextFileResult(FileInfo fileInfo) { /** * Generates and returns a {@link FileResult} with the authorship results from binary {@code fileInfo} consolidated. * Authorship results are indicated in the {@code authorContributionMap} as contributions with zero line counts. - * Returns {@code null} if none of the {@link Author} specified in {@code config} contributed to the file in - * {@code fileInfo}. + * Returns an empty {@code Failable} if none of the {@link Author} specified in + * {@code config} contributed to the file in {@code fileInfo}. */ - private FileResult generateBinaryFileResult(RepoConfiguration config, FileInfo fileInfo) { - List authorsString = GitLog.getFileAuthors(config, fileInfo.getPath()); - if (authorsString.size() == 0) { - return null; - } - + private Failable generateBinaryFileResult( + RepoConfiguration config, FileInfo fileInfo) { Set authors = new HashSet<>(); HashMap authorContributionMap = new HashMap<>(); - for (String[] lineDetails : authorsString) { - String authorName = lineDetails[0]; - String authorEmail = lineDetails[1]; - authors.add(config.getAuthor(authorName, authorEmail)); - } - - for (Author author : authors) { - authorContributionMap.put(author, 0); - } - - return FileResult.createBinaryFileResult(fileInfo.getPath(), fileInfo.getFileType(), authorContributionMap); + return Failable.ofNullable(() -> GitLog.getFileAuthors(config, fileInfo.getPath())) + .filter(x -> !x.isEmpty()) + .ifPresent(x -> { + for (String[] lineDetails : x) { + String authorName = lineDetails[0]; + String authorEmail = lineDetails[1]; + authors.add(config.getAuthor(authorName, authorEmail)); + } + + for (Author author : authors) { + authorContributionMap.put(author, 0); + } + }) + .map(x -> FileResult + .createBinaryFileResult(fileInfo.getPath(), fileInfo.getFileType(), authorContributionMap)); } /** @@ -160,14 +151,14 @@ private void aggregateBlameAuthorModifiedAndDateInfo(RepoConfiguration config, F String authorName = blameResultLines[lineCount + 1].substring(AUTHOR_NAME_OFFSET); String authorEmail = blameResultLines[lineCount + 2] .substring(AUTHOR_EMAIL_OFFSET).replaceAll("<|>", ""); - Long commitDateInMs = Long.parseLong(blameResultLines[lineCount + 3].substring(AUTHOR_TIME_OFFSET)) * 1000; + long commitDateInMs = Long.parseLong(blameResultLines[lineCount + 3].substring(AUTHOR_TIME_OFFSET)) * 1000; LocalDateTime commitDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(commitDateInMs), config.getZoneId()); Author author = config.getAuthor(authorName, authorEmail); if (!fileInfo.isFileLineTracked(lineCount / 5) || author.isIgnoringFile(filePath) || CommitHash.isInsideCommitList(commitHash, config.getIgnoreCommitList()) - || commitDate.compareTo(sinceDate) < 0 || commitDate.compareTo(untilDate) > 0) { + || commitDate.isBefore(sinceDate) || commitDate.isAfter(untilDate)) { author = Author.UNKNOWN_AUTHOR; } diff --git a/src/main/java/reposense/commits/CommitInfoAnalyzer.java b/src/main/java/reposense/commits/CommitInfoAnalyzer.java index 0730e21f7d..764ed1f122 100644 --- a/src/main/java/reposense/commits/CommitInfoAnalyzer.java +++ b/src/main/java/reposense/commits/CommitInfoAnalyzer.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; @@ -25,6 +24,7 @@ import reposense.model.FileType; import reposense.model.RepoConfiguration; import reposense.system.LogsManager; +import reposense.util.function.Failable; /** * Analyzes commit information found in the git log. @@ -89,15 +89,15 @@ public CommitResult analyzeCommit(CommitInfo commitInfo, RepoConfiguration confi Boolean isMergeCommit = elements[PARENT_HASHES_INDEX].split(HASH_SPLITTER).length > 1; Author author = config.getAuthor(elements[AUTHOR_INDEX], elements[EMAIL_INDEX]); - ZonedDateTime date = null; - try { - date = ZonedDateTime.parse(elements[DATE_INDEX], GIT_STRICT_ISO_DATE_FORMAT); - } catch (DateTimeParseException pe) { - logger.log(Level.WARNING, "Unable to parse the date from git log result for commit.", pe); - } - - // Commit date may be in a timezone different from the one given in the config. - LocalDateTime adjustedDate = date.withZoneSameInstant(config.getZoneId()).toLocalDateTime(); + // safe map since ZonedDateTime::now returns non-null + Failable date = Failable + .ofNullable(() -> ZonedDateTime.parse(elements[DATE_INDEX], GIT_STRICT_ISO_DATE_FORMAT), + x -> { + logger.log(Level.WARNING, + "Unable to parse the date from git log result for commit.", x); + return ZonedDateTime.now(); + }) + .map(x -> x.withZoneSameInstant(config.getZoneId()).toLocalDateTime()); String messageTitle = (elements.length > MESSAGE_TITLE_INDEX) ? elements[MESSAGE_TITLE_INDEX] : ""; String messageBody = (elements.length > MESSAGE_BODY_INDEX) @@ -114,7 +114,7 @@ public CommitResult analyzeCommit(CommitInfo commitInfo, RepoConfiguration confi } if (statLine.isEmpty()) { // empty commit, no files changed - return new CommitResult(author, hash, isMergeCommit, adjustedDate, messageTitle, messageBody, tags); + return new CommitResult(author, hash, isMergeCommit, date.get(), messageTitle, messageBody, tags); } String[] statInfos = statLine.split(NEW_LINE_SPLITTER); @@ -122,7 +122,7 @@ public CommitResult analyzeCommit(CommitInfo commitInfo, RepoConfiguration confi Map fileTypeAndContributionMap = getFileTypesAndContribution(fileTypeContributions, config); - return new CommitResult(author, hash, isMergeCommit, adjustedDate, messageTitle, messageBody, tags, + return new CommitResult(author, hash, isMergeCommit, date.get(), messageTitle, messageBody, tags, fileTypeAndContributionMap); } diff --git a/src/main/java/reposense/model/ConfigRunConfiguration.java b/src/main/java/reposense/model/ConfigRunConfiguration.java index e63ca1ea8a..52a4c84981 100644 --- a/src/main/java/reposense/model/ConfigRunConfiguration.java +++ b/src/main/java/reposense/model/ConfigRunConfiguration.java @@ -2,7 +2,7 @@ import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -13,6 +13,7 @@ import reposense.parser.exceptions.InvalidCsvException; import reposense.parser.exceptions.InvalidHeaderException; import reposense.system.LogsManager; +import reposense.util.function.Failable; /** * Represents RepoSense run configured by config files. @@ -38,33 +39,29 @@ public ConfigRunConfiguration(CliArguments cliArguments) { public List getRepoConfigurations() throws IOException, InvalidCsvException, InvalidHeaderException { List repoConfigs = new RepoConfigCsvParser(cliArguments.getRepoConfigFilePath()).parse(); - List authorConfigs; - List groupConfigs; - Path authorConfigFilePath = cliArguments.getAuthorConfigFilePath(); - Path groupConfigFilePath = cliArguments.getGroupConfigFilePath(); + // parse the author config file path + Failable.of(cliArguments::getAuthorConfigFilePath) + .filter(Files::exists) + .map(x -> new AuthorConfigCsvParser(cliArguments.getAuthorConfigFilePath()).parse(), + exception -> { + logger.log(Level.WARNING, exception.getMessage(), exception); + return Collections.emptyList(); + }) + .ifPresent(x -> RepoConfiguration.merge(repoConfigs, x)) + .ifPresent(() -> RepoConfiguration.setHasAuthorConfigFileToRepoConfigs(repoConfigs, true)) + .orElse(Collections.emptyList()); - - if (authorConfigFilePath != null && Files.exists(authorConfigFilePath)) { - try { - authorConfigs = new AuthorConfigCsvParser(cliArguments.getAuthorConfigFilePath()).parse(); - RepoConfiguration.merge(repoConfigs, authorConfigs); - RepoConfiguration.setHasAuthorConfigFileToRepoConfigs(repoConfigs, true); - } catch (IOException | InvalidCsvException e) { - // for all IO and invalid csv exceptions, log the error and continue - logger.log(Level.WARNING, e.getMessage(), e); - } - } - - if (groupConfigFilePath != null && Files.exists(groupConfigFilePath)) { - try { - groupConfigs = new GroupConfigCsvParser(cliArguments.getGroupConfigFilePath()).parse(); - RepoConfiguration.setGroupConfigsToRepos(repoConfigs, groupConfigs); - } catch (IOException | InvalidCsvException e) { - // for all IO and invalid csv exceptions, log the error and continue - logger.log(Level.WARNING, e.getMessage(), e); - } - } + // parse the group config file path + Failable.of(cliArguments::getGroupConfigFilePath) + .filter(Files::exists) + .map(x -> new GroupConfigCsvParser(x).parse(), + exception -> { + logger.log(Level.WARNING, exception.getMessage(), exception); + return Collections.emptyList(); + }) + .ifPresent(x -> RepoConfiguration.setGroupConfigsToRepos(repoConfigs, x)) + .orElse(Collections.emptyList()); return repoConfigs; } diff --git a/src/main/java/reposense/model/RepoConfiguration.java b/src/main/java/reposense/model/RepoConfiguration.java index 8466cfb52b..bc367b04a2 100644 --- a/src/main/java/reposense/model/RepoConfiguration.java +++ b/src/main/java/reposense/model/RepoConfiguration.java @@ -13,7 +13,7 @@ import reposense.git.GitBranch; import reposense.git.exception.GitBranchException; -import reposense.parser.ConfigurationBuildException; +import reposense.parser.exceptions.ConfigurationBuildException; import reposense.system.LogsManager; import reposense.util.FileUtil; diff --git a/src/main/java/reposense/parser/GroupConfigCsvParser.java b/src/main/java/reposense/parser/GroupConfigCsvParser.java index 4a4509a70b..cdb77dc752 100644 --- a/src/main/java/reposense/parser/GroupConfigCsvParser.java +++ b/src/main/java/reposense/parser/GroupConfigCsvParser.java @@ -10,6 +10,7 @@ import reposense.model.GroupConfiguration; import reposense.model.RepoLocation; import reposense.parser.exceptions.InvalidLocationException; +import reposense.util.function.Failable; /** * Container for the values parsed from {@code group-config.csv} file. @@ -57,18 +58,17 @@ protected void processLine(List results, CSVRecord record) t String groupName = get(record, GROUP_NAME_HEADER); List globList = getAsList(record, FILES_GLOB_HEADER); - GroupConfiguration groupConfig = null; - groupConfig = findMatchingGroupConfiguration(results, location); - FileType group = new FileType(groupName, globList); - if (groupConfig.containsGroup(group)) { - logger.warning(String.format( - "Skipping group as %s has already been specified for the repository %s", - group.toString(), groupConfig.getLocation())); - return; - } + GroupConfiguration groupConfig = findMatchingGroupConfiguration(results, location); - groupConfig.addGroup(group); + Failable.success(groupConfig) + .filter(x -> !x.containsGroup(group)) + .ifAbsent(() -> { + logger.warning(String.format( + "Skipping group as %s has already been specified for the repository %s", + group, groupConfig.getLocation())); + }) + .ifPresent(x -> x.addGroup(group)); } /** diff --git a/src/main/java/reposense/parser/ConfigurationBuildException.java b/src/main/java/reposense/parser/exceptions/ConfigurationBuildException.java similarity index 81% rename from src/main/java/reposense/parser/ConfigurationBuildException.java rename to src/main/java/reposense/parser/exceptions/ConfigurationBuildException.java index fd1f43fea8..bb3b302662 100644 --- a/src/main/java/reposense/parser/ConfigurationBuildException.java +++ b/src/main/java/reposense/parser/exceptions/ConfigurationBuildException.java @@ -1,4 +1,4 @@ -package reposense.parser; +package reposense.parser.exceptions; /** * Signals that there was an issue building a Configuration (missing parameters, etc.). diff --git a/src/main/java/reposense/util/function/Failable.java b/src/main/java/reposense/util/function/Failable.java new file mode 100644 index 0000000000..e7393f2d77 --- /dev/null +++ b/src/main/java/reposense/util/function/Failable.java @@ -0,0 +1,439 @@ +package reposense.util.function; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * Represents a task type that encapsulates information about running a task with the possibility of + * throwing exception. + * + * @param Generic input type {@code T} + */ +public abstract class Failable { + // Empty instance + private static final Failable EMPTY = new Empty(); + + /** + * Returns an instance of {@code Failable}. Accepts a {@code ThrowableSupplier} + * and produces an item of type {@code T}. If it fails, then a failed instance of + * {@code Failable} is returned. This method allows {@code null} to be stored within + * it. + * + * @param Generic type {@code T}. + * @param Generic type {@code E} bounded by {@code Throwable}. + * @param supplier Produces objects of type {@code T}. + * @return Successful instance of {@code Failable} if the {@code ThrowableSupplier} + * runs without failure or a failed instance of {@code Failable}. + */ + public static Failable of( + ThrowableSupplier supplier, Function exceptionHandler) { + try { + return Failable.success(supplier.produce()); + } catch (Throwable throwable) { + // ensured by type declaration above + @SuppressWarnings("unchecked") + E exception = (E) throwable; + T recoveredItem = exceptionHandler.apply(exception); + return Failable.success(recoveredItem); + } + } + + /** + * Returns an instance of {@code Failable}. Accepts a {@code Supplier} + * and produces an item of type {@code T}. This method allows {@code null} to + * be stored within it. + * + * @param Generic type {@code T}. + * @param supplier Produces objects of type {@code T}. + * @return Successful instance of {@code Failable} if + * the {@code ThrowableSupplier} runs without failure or a failed instance + * of {@code Failable}. + */ + public static Failable of(Supplier supplier) { + return Failable.success(supplier.get()); + } + + /** + * Returns an instance of {@code Failable}. Accepts a {@code ThrowableSupplier} + * and produces an item of type {@code T}. If it fails, then a failed instance of + * {@code Failable} is returned. This method converts {@code null} objects + * into empty instances of {@code Failable}. + * + * @param Generic type {@code T}. + * @param Generic type {@code E} bounded by {@code Throwable}. + * @param supplier Produces objects of type {@code T}. + * @return Successful instance of {@code Failable} if the {@code ThrowableSupplier} + * runs without failure, or empty instance of {@code Failable} if {@code null} + * is produced, or a failed instance of {@code Failable}. + */ + public static Failable ofNullable( + ThrowableSupplier supplier, Function exceptionHandler) { + try { + T returns = supplier.produce(); + + if (returns == null) { + return Failable.empty(); + } + + return Failable.success(returns); + } catch (Throwable throwable) { + // safe as type guaranteed by function declaration + @SuppressWarnings("unchecked") + E exception = (E) throwable; + T item = exceptionHandler.apply(exception); + + return Failable.ofNullable(item); + } + } + + /** + * Creates a successful instance of {@code Failable}, or returns an empty instance + * if item is {@code null}. + * + * @param Generic type {@code T}. + * @param supplier Provides an item of type {@code T} to store. + * @return Successful instance of {@code Failable} if item is not {@code null} else + * empty instance of {@code Failable}. + */ + public static Failable ofNullable(Supplier supplier) { + return Failable.ofNullable(supplier.get()); + } + + /** + * Creates a successful instance of {@code Failable}, or returns an empty instance + * if item is {@code null}. + * + * @param Generic type {@code T}. + * @param item Item of type {@code T} to store. + * @return Successful instance of {@code Failable} if item is not {@code null} else + * empty instance of {@code Failable}. + */ + public static Failable ofNullable(T item) { + if (item == null) { + return Failable.empty(); + } + + return Failable.success(item); + } + + /** + * Creates a successful instance of {@code Failable}. + * + * @param Generic type {@code T}. + * @param item Item of type {@code T} to store. + * @return Successful instance of {@code Failable}. + */ + public static Failable success(T item) { + return new Success<>(item); + } + + /** + * Creates an empty instance of {@code Failable}. + * + * @param Generic type {@code T}. + * @return Empty instance of {@code Failable}. + */ + public static Failable empty() { + // safe as empty contains nothing, and no monadic actions will cause it to turn into anything else + @SuppressWarnings("unchecked") + Failable failed = (Failable) Failable.EMPTY; + return failed; + } + + /** + * Returns the item stored in this {@code Failable} instance. + * + * @return Item of type {@code T}, or throws an exception if there is no item stored. + */ + public abstract T get(); + + /** + * Returns the item stored in this {@code Failable} instance, or the input item if this + * instance does not contain any items. + * + * @param item Item of type {@code T} to return if this instance has failed or is empty. + * @return This instance's item of type {@code T}, or the input item of type {@code T}. + */ + public abstract T orElse(T item); + + /** + * Tests the item stored in this instance against some {@code Predicate}. + * + * @param predicate {@code Predicate} to test this instance's item against. + * @return This instance if the predicate test passses, else an empty {@code Failable} instance. + */ + public abstract Failable filter(Predicate predicate); + + /** + * Maps this instance's item to a new item of type {@code U}. The mapping function cannot fail + * (throw exceptions). + * + * @param function {@code Function} that accepts an object of type {@code T} and returns a new + * object of type {@code U}. + * @param Generic type {@code U}. + * @return {@code Failable} instance + */ + public abstract Failable map(Function function); + + /** + * Maps this instance's item to a new item of type {@code U}. The mapping function may fail + * and throw an exception. + * + * @param throwableFunction {@code Function} that accepts an object of type {@code T} and returns a new + * object of type {@code U}. This function may fail and throw an exception. + * @param Generic type {@code Z} bounded by {@code Throwable}. + * @param Generic type {@code U}. + * @return {@code Failable} instance + */ + public abstract Failable map( + ThrowableFunction throwableFunction, + Function exceptionHandler); + + /** + * Maps this instance's item to a new {@code Failable} object. The mapping function cannot fail + * (throw exceptions). + * + * @param function {@code Function} that accepts an object of type {@code T} and returns a + * new {@code Failable} object. + * @param Generic type {@code U}. + * @return {@code Failable} instance + */ + public abstract Failable flatMap( + Function> function); + + /** + * Maps this instance's item to a new {@code Failable} object. The mapping function may fail + * and throw an exception of the same type {@code E}. + * + * @param throwableFunction {@code Function} that accepts an object of type {@code T} and returns a + * new {@code Failable} object. This function may fail and throw an exception. + * @param Generic type {@code U}. + * @return {@code Failable} instance + */ + public abstract Failable flatMap( + ThrowableFunction, E> throwableFunction, + Function> exceptionHandler); + + /** + * Checks if this instance is a present instance of {@code Failable}. + * + * @return true if this instance is an instance of {@code Failable} else false. + */ + public abstract boolean isPresent(); + + /** + * Checks if this instance is an absent instance of {@code Failable}. + * + * @return true if this instance is an absent instance of {@code Failable} else false. + */ + public abstract boolean isAbsent(); + + /** + * Executes a {@code Runnable} object if this instance is a present instance of {@code Failable}. + * + * @param runner {@code Runnable} object to run. + * @return This instance. + */ + public abstract Failable ifPresent(Runnable runner); + + /** + * Consumes the item stored in this instance if this instance is a present instance of {@code Failable}. + * + * @param consumer {@code Consumer} object to consume the item stored in this instance. + * @return This instance. + */ + public abstract Failable ifPresent(Consumer consumer); + + /** + * Executes a {@code Runnable} object if this instance is a absent instance of {@code Failable}. + * + * @param runner {@code Runnable} object to run. + * @return This instance. + */ + public abstract Failable ifAbsent(Runnable runner); + + /** + * Successful instance of {@code Failable}. + * + * @param Generic type {@code T}. + */ + private static final class Success extends Failable { + private final T item; + + private Success(T item) { + this.item = item; + } + + @Override + public T get() { + return this.item; + } + + @Override + public T orElse(T item) { + return this.item; + } + + @Override + public Failable filter(Predicate predicate) { + if (predicate.test(this.item)) { + return this; + } + + return Failable.empty(); + } + + @Override + public Failable map(Function function) { + return Failable.of(() -> function.apply(this.item)); + } + + @Override + public Failable map( + ThrowableFunction throwableFunction, + Function exceptionHandler) { + return Failable.of(() -> throwableFunction.apply(this.item), exceptionHandler); + } + + @Override + public Failable flatMap(Function> function) { + return function.apply(this.item); + } + + @Override + public Failable flatMap( + ThrowableFunction, E> throwableFunction, + Function> exceptionHandler) { + try { + return throwableFunction.apply(this.item); + } catch (Throwable throwable) { + @SuppressWarnings("unchecked") + E exception = (E) throwable; + return (exceptionHandler.apply(exception)); + } + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public boolean isAbsent() { + return false; + } + + @Override + public Failable ifPresent(Runnable runner) { + runner.run(); + return this; + } + + @Override + public Failable ifPresent(Consumer consumer) { + consumer.accept(this.item); + return this; + } + + @Override + public Failable ifAbsent(Runnable runner) { + return this; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof Success) { + Success success = (Success) obj; + return success.item.equals(this.item); + } + + return false; + } + } + + /** + * Empty instance of {@code Failable}. + */ + private static final class Empty extends Failable { + private Empty() { + + } + + @Override + public Object get() { + throw new NoSuchElementException("Empty instance of Failable contains no items"); + } + + @Override + public Object orElse(Object item) { + return item; + } + + @Override + public Failable filter(Predicate predicate) { + return Failable.empty(); + } + + @Override + public Failable map(Function function) { + return Failable.empty(); + } + + @Override + public Failable map( + ThrowableFunction throwableFunction, + Function exceptionHandler) { + return Failable.empty(); + } + + @Override + public Failable flatMap(Function> function) { + return Failable.empty(); + } + + @Override + public Failable flatMap( + ThrowableFunction, E> throwableFunction, + Function> exceptionHandler) { + return Failable.empty(); + } + + @Override + public boolean isPresent() { + return false; + } + + @Override + public boolean isAbsent() { + return true; + } + + @Override + public Failable ifPresent(Runnable runner) { + return this; + } + + @Override + public Failable ifPresent(Consumer consumer) { + return this; + } + + @Override + public Failable ifAbsent(Runnable runner) { + runner.run(); + return this; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Empty; + } + } +} diff --git a/src/main/java/reposense/util/function/ThrowableConsumer.java b/src/main/java/reposense/util/function/ThrowableConsumer.java new file mode 100644 index 0000000000..3b57fdabf2 --- /dev/null +++ b/src/main/java/reposense/util/function/ThrowableConsumer.java @@ -0,0 +1,15 @@ +package reposense.util.function; + +/** + * Functional interface that defines a Consumer that can throw + * an Exception on execution. + * + * @param The Type of the item that this {@code ThrowableConsumer} can + * consume. + * @param The Type of the Exception that this {@code ThrowableConsumer} + * can throw. + */ +@FunctionalInterface +public interface ThrowableConsumer { + void consume(T t) throws E; +} diff --git a/src/main/java/reposense/util/function/ThrowableFunction.java b/src/main/java/reposense/util/function/ThrowableFunction.java new file mode 100644 index 0000000000..b0d828bf13 --- /dev/null +++ b/src/main/java/reposense/util/function/ThrowableFunction.java @@ -0,0 +1,15 @@ +package reposense.util.function; + +/** + * Functional interface that defines a Supplier that can throw + * an Exception on execution. + * + * @param The Input Type of the item that this {@code ThrowableFunction}. + * @param The Return Type of this {@code ThrowableFunction}. + * @param The Type of the Exception that this {@code ThrowableFunction}. + * can throw. + */ +@FunctionalInterface +public interface ThrowableFunction { + U apply(T t) throws E; +} diff --git a/src/main/java/reposense/util/function/ThrowableSupplier.java b/src/main/java/reposense/util/function/ThrowableSupplier.java new file mode 100644 index 0000000000..afad6da1a2 --- /dev/null +++ b/src/main/java/reposense/util/function/ThrowableSupplier.java @@ -0,0 +1,15 @@ +package reposense.util.function; + +/** + * Functional interface that defines a Supplier that can throw + * an Exception on execution. + * + * @param The Type of the item that this {@code ThrowableSupplier} can + * supply. + * @param The Type of the Exception that this {@code ThrowableSupplier} + * can throw. + */ +@FunctionalInterface +public interface ThrowableSupplier { + T produce() throws E; +} diff --git a/src/test/java/reposense/authorship/AnnotatorAnalyzerTest.java b/src/test/java/reposense/authorship/AnnotatorAnalyzerTest.java index 092a23035f..9b2f66ee0a 100644 --- a/src/test/java/reposense/authorship/AnnotatorAnalyzerTest.java +++ b/src/test/java/reposense/authorship/AnnotatorAnalyzerTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Test; import reposense.authorship.analyzer.AnnotatorAnalyzer; -import reposense.authorship.model.FileResult; import reposense.model.Author; import reposense.model.AuthorConfiguration; import reposense.model.RepoConfiguration; @@ -66,23 +65,39 @@ public void after() { @Test public void analyzeAnnotation_authorNamePresentInConfig_overrideAuthorship() { config.setAuthorList(new ArrayList<>(Arrays.asList(FAKE_AUTHOR))); - FileResult fileResult = getFileResult("annotationTest.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_OVERRIDE_AUTHORSHIP_TEST)); + getFileResult("annotationTest.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_OVERRIDE_AUTHORSHIP_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test public void analyzeAnnotation_authorNameNotInConfigAndNoAuthorConfigFile_acceptTaggedAuthor() { config.setAuthorList(new ArrayList<>(Arrays.asList(FAKE_AUTHOR))); - FileResult fileResult = getFileResult("annotationTest.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_OVERRIDE_AUTHORSHIP_TEST)); + getFileResult("annotationTest.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_OVERRIDE_AUTHORSHIP_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test public void analyzeAnnotation_authorNameNotInConfigAndHaveAuthorConfigFile_disownCode() { config.setAuthorList(new ArrayList<>(Arrays.asList(FAKE_AUTHOR))); config.setHasAuthorConfigFile(true); - FileResult fileResult = getFileResult("annotationTest.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_DISOWN_CODE_TEST)); + getFileResult("annotationTest.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_DISOWN_CODE_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); + } @Test diff --git a/src/test/java/reposense/authorship/FileAnalyzerTest.java b/src/test/java/reposense/authorship/FileAnalyzerTest.java index 77c5db5819..638474cab7 100644 --- a/src/test/java/reposense/authorship/FileAnalyzerTest.java +++ b/src/test/java/reposense/authorship/FileAnalyzerTest.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.Test; import reposense.authorship.model.FileInfo; -import reposense.authorship.model.FileResult; import reposense.git.GitCheckout; import reposense.model.Author; import reposense.model.CommitHash; @@ -88,8 +87,13 @@ public void before() throws Exception { public void blameTest() { config.setSinceDate(BLAME_TEST_SINCE_DATE); config.setUntilDate(BLAME_TEST_UNTIL_DATE); - FileResult fileResult = getFileResult("blameTest.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_BLAME_TEST)); + getFileResult("blameTest.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_BLAME_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test @@ -102,18 +106,27 @@ public void blameWithPreviousAuthorsTest() { GitCheckout.checkout(config.getRepoRoot(), TEST_REPO_BLAME_WITH_PREVIOUS_AUTHORS_BRANCH); createTestIgnoreRevsFile(AUTHOR_TO_IGNORE_BLAME_COMMIT_LIST_07082021); - FileResult fileResult = getFileResult("blameTest.java"); - removeTestIgnoreRevsFile(); - - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_PREVIOUS_AUTHORS_BLAME_TEST)); + getFileResult("blameTest.java") + .ifPresent(x -> { + removeTestIgnoreRevsFile(); + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_PREVIOUS_AUTHORS_BLAME_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test public void movedFileBlameTest() { config.setSinceDate(MOVED_FILE_SINCE_DATE); config.setUntilDate(MOVED_FILE_UNTIL_DATE); - FileResult fileResult = getFileResult("newPos/movedFile.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_MOVED_FILE)); + getFileResult("newPos/movedFile.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_MOVED_FILE)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test @@ -121,9 +134,13 @@ public void blameTestDateRange() throws Exception { GitCheckout.checkoutDate(config.getRepoRoot(), config.getBranch(), BLAME_TEST_UNTIL_DATE, config.getZoneId()); config.setSinceDate(BLAME_TEST_SINCE_DATE); config.setUntilDate(BLAME_TEST_UNTIL_DATE); - - FileResult fileResult = getFileResult("blameTest.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_BLAME_TEST)); + getFileResult("blameTest.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_BLAME_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test @@ -138,10 +155,14 @@ public void blameWithPreviousAuthorsTestDateRange() throws Exception { config.getZoneId()); createTestIgnoreRevsFile(AUTHOR_TO_IGNORE_BLAME_COMMIT_LIST_07082021); - FileResult fileResult = getFileResult("blameTest.java"); - removeTestIgnoreRevsFile(); - - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_PREVIOUS_AUTHORS_BLAME_TEST)); + getFileResult("blameTest.java") + .ifPresent(x -> { + removeTestIgnoreRevsFile(); + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_PREVIOUS_AUTHORS_BLAME_TEST)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test @@ -149,9 +170,13 @@ public void movedFileBlameTestDateRange() throws Exception { GitCheckout.checkoutDate(config.getRepoRoot(), config.getBranch(), MOVED_FILE_UNTIL_DATE, config.getZoneId()); config.setSinceDate(MOVED_FILE_SINCE_DATE); config.setUntilDate(MOVED_FILE_UNTIL_DATE); - - FileResult fileResult = getFileResult("newPos/movedFile.java"); - assertFileAnalysisCorrectness(fileResult, Arrays.asList(EXPECTED_LINE_AUTHORS_MOVED_FILE)); + getFileResult("newPos/movedFile.java") + .ifPresent(x -> { + assertFileAnalysisCorrectness(x, Arrays.asList(EXPECTED_LINE_AUTHORS_MOVED_FILE)); + }) + .ifAbsent(() -> { + throw new AssertionError(); + }); } @Test @@ -372,7 +397,7 @@ public void analyzeBinaryFile_nonExistingFilePath_success() { new FileInfo("/nonExistingPngPicture.png")); for (FileInfo binaryFileInfo: binaryFileInfos) { - Assertions.assertNull(fileInfoAnalyzer.analyzeBinaryFile(config, binaryFileInfo)); + Assertions.assertTrue(fileInfoAnalyzer.analyzeBinaryFile(config, binaryFileInfo).isAbsent()); } } diff --git a/src/test/java/reposense/authorship/FileResultAggregatorTest.java b/src/test/java/reposense/authorship/FileResultAggregatorTest.java index 282265a6b1..e43ae32fe0 100644 --- a/src/test/java/reposense/authorship/FileResultAggregatorTest.java +++ b/src/test/java/reposense/authorship/FileResultAggregatorTest.java @@ -4,7 +4,6 @@ import java.time.Month; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -18,6 +17,7 @@ import reposense.model.RepoConfiguration; import reposense.template.GitTestTemplate; import reposense.util.TestUtil; +import reposense.util.function.Failable; public class FileResultAggregatorTest extends GitTestTemplate { @@ -50,9 +50,9 @@ public void aggregateFileResult_clearFileLines_success() { List fileResults = textFileInfos.stream() .filter(f -> !f.getPath().equals("annotationTest.java")) .map(fileInfo -> fileInfoAnalyzer.analyzeTextFile(config, fileInfo)) - .filter(Objects::nonNull) + .filter(Failable::isPresent) + .map(Failable::get) .collect(Collectors.toList()); - // FileResultAggregator fileResultAggregator = new FileResultAggregator(); fileResultAggregator.aggregateFileResult(fileResults, config.getAuthorList(), config.getAllFileTypes()); diff --git a/src/test/java/reposense/model/RepoConfigurationTest.java b/src/test/java/reposense/model/RepoConfigurationTest.java index 60b2892660..6723a0d641 100644 --- a/src/test/java/reposense/model/RepoConfigurationTest.java +++ b/src/test/java/reposense/model/RepoConfigurationTest.java @@ -18,9 +18,9 @@ import reposense.parser.ArgsParser; import reposense.parser.AuthorConfigCsvParser; -import reposense.parser.ConfigurationBuildException; import reposense.parser.GroupConfigCsvParser; import reposense.parser.RepoConfigCsvParser; +import reposense.parser.exceptions.ConfigurationBuildException; import reposense.report.ReportGenerator; import reposense.util.InputBuilder; import reposense.util.TestRepoCloner; diff --git a/src/test/java/reposense/template/GitTestTemplate.java b/src/test/java/reposense/template/GitTestTemplate.java index 1b9b853588..aec2ea61a4 100644 --- a/src/test/java/reposense/template/GitTestTemplate.java +++ b/src/test/java/reposense/template/GitTestTemplate.java @@ -30,6 +30,7 @@ import reposense.model.RepoLocation; import reposense.util.FileUtil; import reposense.util.TestRepoCloner; +import reposense.util.function.Failable; /** * Contains templates for git testing. @@ -188,7 +189,7 @@ public void assertFileAnalysisCorrectness(FileResult fileResult, List ex } } - public FileResult getFileResult(String relativePath) { + public Failable getFileResult(String relativePath) { FileInfo fileInfo = fileInfoExtractor.generateFileInfo(configs.get(), relativePath); return fileInfoAnalyzer.analyzeTextFile(configs.get(), fileInfo); } diff --git a/src/test/java/reposense/util/FailableTest.java b/src/test/java/reposense/util/FailableTest.java new file mode 100644 index 0000000000..01012f7d0a --- /dev/null +++ b/src/test/java/reposense/util/FailableTest.java @@ -0,0 +1,189 @@ +package reposense.util; + +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import reposense.util.function.Failable; + +public class FailableTest { + private static final Failable PRESENT = Failable.success(123); + private static final Failable ABSENT = Failable.empty(); + + @Test + public void isPresent_testAll_success() { + Assertions.assertTrue(PRESENT.isPresent()); + Assertions.assertFalse(ABSENT.isPresent()); + } + + @Test + public void isAbsent_testAll_success() { + Assertions.assertFalse(PRESENT.isAbsent()); + Assertions.assertTrue(ABSENT.isAbsent()); + } + + @Test + public void of_nullItem_success() { + Assertions.assertNull(Failable.success(null).get()); + } + + @Test + public void of_nonNullItem_success() { + Assertions.assertEquals(Failable.success("hello world").get(), "hello world"); + } + + @Test + public void of_nullItemSupplier_success() { + Assertions.assertNull(Failable.success(null).get()); + } + + @Test + public void of_nonNullItemSupplier_success() { + Assertions.assertEquals(Failable.success(1234).get(), 1234); + } + + @Test + public void of_thrownException_recovered() { + Assertions.assertEquals(Failable.of(() -> { + throw new IllegalAccessError(); + }, x -> 1).get(), 1); + } + + @Test + public void ofNullable_nullItem_success() { + Assertions.assertEquals(Failable.ofNullable(() -> null), Failable.empty()); + } + + @Test + public void ofNullable_nonNullItem_success() { + Assertions.assertEquals(Failable.ofNullable(() -> "hello world").get(), "hello world"); + } + + @Test + public void ofNullable_nullItemSupplier_success() { + Assertions.assertEquals(Failable.ofNullable(() -> null), Failable.empty()); + } + + @Test + public void ofNullable_nonNullItemSupplier_success() { + Assertions.assertEquals(Failable.ofNullable(() -> 1234).get(), 1234); + } + + @Test + public void ofNullable_thrownException_recovered() { + Assertions.assertEquals(Failable.ofNullable(() -> { + throw new IllegalAccessError(); + }, x -> 1).get(), 1); + } + + @Test + public void ofAbsent_returnsSameInstance_success() { + Assertions.assertSame(Failable.empty(), Failable.empty()); + } + + @Test + public void ofAbsent_noStoredValueCheck_success() { + Assertions.assertThrows(NoSuchElementException.class, () -> Failable.empty().get()); + } + + @Test + public void ifPresent_testAll_success() { + int[] testArray = {-1, -1}; + + PRESENT.ifPresent(() -> testArray[0] = 1); + ABSENT.ifPresent(() -> testArray[1] = 1); + + Assertions.assertArrayEquals(testArray, new int[]{1, -1}); + } + + @Test + public void ifAbsent_testAll_success() { + int[] testArray = {-1, -1}; + + PRESENT.ifAbsent(() -> testArray[0] = 1); + ABSENT.ifAbsent(() -> testArray[1] = 1); + + Assertions.assertArrayEquals(testArray, new int[]{-1, 1}); + } + @Test + public void map_testAll_success() { + Assertions.assertEquals(PRESENT.map(x -> "String").get(), "String"); + Assertions.assertEquals(ABSENT.map(x -> "String"), ABSENT); + } + + @Test + public void map_throwingFunctionTestAll_success() { + Assertions.assertInstanceOf(PRESENT.getClass(), PRESENT.map(x -> { + throw new Exception(); + }, x -> 1000)); + Assertions.assertInstanceOf(ABSENT.getClass(), ABSENT.map(x -> { + throw new Exception(); + }, x -> 1)); + } + + @Test + public void unfailableMap_testAll_success() { + Assertions.assertEquals(PRESENT.map(x -> "String").get(), "String"); + Assertions.assertEquals(ABSENT.map(x -> "String"), ABSENT); + } + + @Test + public void flatMap_testAll_success() { + Assertions.assertEquals(PRESENT.flatMap(x -> Failable.success("String")).get(), "String"); + Assertions.assertEquals(ABSENT.flatMap(x -> Failable.success("String")), ABSENT); + } + + @Test + public void flatMap_throwingFunctionTestAll_success() { + Assertions.assertInstanceOf(PRESENT.getClass(), PRESENT.flatMap(x -> Failable.success(2))); + Assertions.assertInstanceOf(ABSENT.getClass(), PRESENT.flatMap(x -> Failable.empty())); + + Assertions.assertInstanceOf(ABSENT.getClass(), ABSENT.map(x -> { + throw new Exception(); + }, x -> 100)); + } + + @Test + public void unfailableFlatMap_testAll_success() { + Assertions.assertEquals(PRESENT.flatMap(x -> Failable.success("String")).get(), "String"); + Assertions.assertEquals(ABSENT.flatMap(x -> Failable.success("String")), ABSENT); + } + + @Test + public void filter_presentFailable_passPredicate() { + Assertions.assertSame(PRESENT.filter(x -> x > 0), PRESENT); + } + + @Test + public void filter_absentFailable_success() { + Assertions.assertSame(ABSENT.filter(x -> x < 0), ABSENT); + } + + @Test + public void get_testAll_success() { + Assertions.assertEquals(PRESENT.get(), 123); + Assertions.assertThrows(NoSuchElementException.class, ABSENT::get); + } + + @Test + public void orElse_testAll_success() { + Assertions.assertEquals(PRESENT.orElse(999), 123); + Assertions.assertEquals(ABSENT.orElse(999), 999); + } + + @Test + public void equals_presentFailable_multipleTests() { + Assertions.assertSame(PRESENT, PRESENT); + Assertions.assertEquals(PRESENT, PRESENT); + Assertions.assertNotEquals(PRESENT, ABSENT); + Assertions.assertNotEquals(PRESENT, Failable.success("String")); + } + + @Test + public void equals_absentFailable_multipleTests() { + Assertions.assertSame(ABSENT, ABSENT); + Assertions.assertEquals(ABSENT, ABSENT); + Assertions.assertNotEquals(ABSENT, PRESENT); + } +}