diff --git a/README.md b/README.md
index c25cad20..2f70a292 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
A time recording tool
[![Build](https://github.com/itsallcode/white-rabbit/workflows/Build/badge.svg)](https://github.com/itsallcode/white-rabbit/actions?query=workflow%3ABuild)
-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=white-rabbit&metric=alert_status)](https://sonarcloud.io/dashboard?id=white-rabbit)
-[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=white-rabbit&metric=coverage)](https://sonarcloud.io/dashboard?id=white-rabbit)
+[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=org.itsallcode.whiterabbit%3Awhite-rabbit&metric=alert_status)](https://sonarcloud.io/dashboard?id=org.itsallcode.whiterabbit%3Awhite-rabbit)
+[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=org.itsallcode.whiterabbit%3Awhite-rabbit&metric=coverage)](https://sonarcloud.io/dashboard?id=org.itsallcode.whiterabbit%3Awhite-rabbit)
* [Features](#features)
* [Usage](#usage)
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/Plugin.java b/api/src/main/java/org/itsallcode/whiterabbit/api/Plugin.java
index 1704a9df..4e86e64f 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/Plugin.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/Plugin.java
@@ -1,14 +1,64 @@
package org.itsallcode.whiterabbit.api;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
+
+/**
+ * Implement this interface to create a plugin for WhiteRabbit. Register your
+ * plugin by adding file
+ * {@code META-INF/services/org.itsallcode.whiterabbit.api.Plugin} containing
+ * the full qualified class name to the jar.
+ *
+ * Available features:
+ *
+ * - {@link ProjectReportExporter}
+ * - {@link MonthDataStorage}
+ *
+ */
public interface Plugin
{
+ /**
+ * Called once when loading the plugin.
+ *
+ * @param config
+ * the configuration of the plugin.
+ */
void init(PluginConfiguration config);
+ /**
+ * The ID of this plugin. IDs must be unique for all plugins. The ID is used
+ * as prefix for {@link PluginConfiguration#getMandatoryValue(String)}.
+ *
+ * @return the ID of this plugin.
+ */
String getId();
- void close();
+ /**
+ * Check if this plugin supports the given feature.
+ *
+ * @param featureType
+ * the feature type.
+ * @return true
if this plugin supports the given feature, else
+ * false
.
+ */
+ boolean supports(Class extends PluginFeature> featureType);
- boolean supports(Class> featureType);
+ /**
+ * Get an instance of the given feature type.
+ *
+ * @param featureType
+ * the feature type.
+ * @param
+ * the type of the feature.
+ * @return the instance of the given feature. May return null
+ * or throw an exception if the feature is not supported.
+ */
+ T getFeature(Class featureType);
- T getFeature(Class featureType);
+ /**
+ * Called before closing the plugin. The plugin should cleanup any resources
+ * in this method.
+ */
+ void close();
}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/PluginConfiguration.java b/api/src/main/java/org/itsallcode/whiterabbit/api/PluginConfiguration.java
index fc882823..05ba3caa 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/PluginConfiguration.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/PluginConfiguration.java
@@ -1,6 +1,21 @@
package org.itsallcode.whiterabbit.api;
+/**
+ * The configuration of a {@link Plugin} that allows retrieving configuration
+ * values from the WhiteRabbit properties file
+ * {@code ~/.whiterabbit.properties}.
+ */
public interface PluginConfiguration
{
- String getMandatoryValue(String string);
+ /**
+ * Get property {@code pluginId.propertyName} from the config file. Throws
+ * an exception if the property is not available.
+ *
+ * @param propertyName
+ * the property name to get.
+ * @return the value of the property.
+ * @throws RuntimeException
+ * if the property is not available in the config file.
+ */
+ String getMandatoryValue(String propertyName);
}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/ProgressMonitor.java b/api/src/main/java/org/itsallcode/whiterabbit/api/ProgressMonitor.java
deleted file mode 100644
index 9a9425e3..00000000
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/ProgressMonitor.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.itsallcode.whiterabbit.api;
-
-public interface ProgressMonitor
-{
- boolean isCanceled();
-
- void beginTask(String name, int totalWork);
-
- void setTaskName(String name);
-
- void worked(int work);
-}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/ProjectReportExporter.java b/api/src/main/java/org/itsallcode/whiterabbit/api/ProjectReportExporter.java
deleted file mode 100644
index e55263dc..00000000
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/ProjectReportExporter.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.itsallcode.whiterabbit.api;
-
-import org.itsallcode.whiterabbit.api.model.ProjectReport;
-
-public interface ProjectReportExporter
-{
- void export(ProjectReport report, ProgressMonitor progressMonitor);
-}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/features/MonthDataStorage.java b/api/src/main/java/org/itsallcode/whiterabbit/api/features/MonthDataStorage.java
new file mode 100644
index 00000000..df376ae4
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/features/MonthDataStorage.java
@@ -0,0 +1,71 @@
+package org.itsallcode.whiterabbit.api.features;
+
+import java.time.YearMonth;
+import java.util.List;
+import java.util.Optional;
+
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
+
+/**
+ * {@link PluginFeature} that provides a storage backend for month data. This
+ * class may keep the data on the local disk or on a backend server.
+ * {@link YearMonth} is used as key for loading and storing entries.
+ */
+public interface MonthDataStorage extends PluginFeature
+{
+ /**
+ * @param month
+ * the month for which to load the data.
+ * @return the data for the given month.
+ */
+ Optional load(YearMonth month);
+
+ /**
+ * Store the data for a given month.
+ *
+ * @param month
+ * the month.
+ * @param data
+ * the data to store.
+ */
+ void store(YearMonth month, MonthData data);
+
+ /**
+ * @return all months for that data is available in the store.
+ */
+ List getAvailableDataMonths();
+
+ /**
+ * @return all available data.
+ */
+ List loadAll();
+
+ /**
+ * @return the model factory that can be used to create instances of the
+ * model.
+ */
+ ModelFactory getModelFactory();
+
+ /**
+ * A {@link ModelFactory} allows creating new instances of the data model.
+ */
+ public interface ModelFactory
+ {
+ /**
+ * @return a new {@link MonthData} instance.
+ */
+ MonthData createMonthData();
+
+ /**
+ * @return a new {@link DayData} instance.
+ */
+ DayData createDayData();
+
+ /**
+ * @return a new {@link ActivityData} instance.
+ */
+ ActivityData createActivityData();
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/features/PluginFeature.java b/api/src/main/java/org/itsallcode/whiterabbit/api/features/PluginFeature.java
new file mode 100644
index 00000000..85060dac
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/features/PluginFeature.java
@@ -0,0 +1,11 @@
+package org.itsallcode.whiterabbit.api.features;
+
+import org.itsallcode.whiterabbit.api.Plugin;
+
+/**
+ * Super interface for all features supported by a {@link Plugin}.
+ */
+public interface PluginFeature
+{
+
+}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProgressMonitor.java b/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProgressMonitor.java
new file mode 100644
index 00000000..28143ab5
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProgressMonitor.java
@@ -0,0 +1,41 @@
+package org.itsallcode.whiterabbit.api.features;
+
+/**
+ * A progress monitor passed to a {@link PluginFeature} that allows reporting
+ * the progress of process.
+ */
+public interface ProgressMonitor
+{
+ /**
+ * Call this method repeatedly to check, if the user wants to cancel the
+ * process. Abort the process if this returns true
.
+ *
+ * @return true
if the user has cancelled the process.
+ */
+ boolean isCanceled();
+
+ /**
+ * Notifies that the main task is beginning.
+ *
+ * @param name
+ * the name of the current task shown to the user.
+ * @param totalWork
+ * the total number of work units that will be done by the task.
+ */
+ void beginTask(String name, int totalWork);
+
+ /**
+ * @param name
+ * the name of the current task shown to the user.
+ */
+ void setTaskName(String name);
+
+ /**
+ * Notifies that a given number of work unit of the main task has been
+ * completed.
+ *
+ * @param work
+ * the number of work units just completed.
+ */
+ void worked(int work);
+}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProjectReportExporter.java b/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProjectReportExporter.java
new file mode 100644
index 00000000..b29afbae
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/features/ProjectReportExporter.java
@@ -0,0 +1,20 @@
+package org.itsallcode.whiterabbit.api.features;
+
+import org.itsallcode.whiterabbit.api.model.ProjectReport;
+
+/**
+ * A {@link PluginFeature} that allows exporting a monthly {@link ProjectReport}
+ * to another system.
+ */
+public interface ProjectReportExporter extends PluginFeature
+{
+ /**
+ * Start the export.
+ *
+ * @param report
+ * the report to export.
+ * @param progressMonitor
+ * a progress monitor for reporting the export progress.
+ */
+ void export(ProjectReport report, ProgressMonitor progressMonitor);
+}
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ActivityData.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ActivityData.java
new file mode 100644
index 00000000..1d477e17
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ActivityData.java
@@ -0,0 +1,24 @@
+package org.itsallcode.whiterabbit.api.model;
+
+import java.time.Duration;
+
+/**
+ * Model for a project activity on a given {@link DayData day} including project
+ * id, duration and comment.
+ */
+public interface ActivityData
+{
+ String getProjectId();
+
+ void setProjectId(String id);
+
+ Duration getDuration();
+
+ boolean isRemainder();
+
+ void setDuration(Duration duration);
+
+ String getComment();
+
+ void setComment(String comment);
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayData.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayData.java
new file mode 100644
index 00000000..1fbe72a3
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayData.java
@@ -0,0 +1,45 @@
+package org.itsallcode.whiterabbit.api.model;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * Model class representing a day with date, time for begin and end of work,
+ * activities etc.
+ */
+public interface DayData
+{
+ LocalDate getDate();
+
+ DayType getType();
+
+ LocalTime getBegin();
+
+ LocalTime getEnd();
+
+ Duration getInterruption();
+
+ Duration getWorkingHours();
+
+ String getComment();
+
+ void setDate(LocalDate date);
+
+ void setType(DayType type);
+
+ void setBegin(LocalTime begin);
+
+ void setEnd(LocalTime end);
+
+ void setWorkingHours(Duration workingHours);
+
+ void setComment(String comment);
+
+ void setInterruption(Duration interruption);
+
+ List getActivities();
+
+ void setActivities(List activities);
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayType.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayType.java
index 323a24a8..24f9e152 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayType.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/DayType.java
@@ -1,5 +1,8 @@
package org.itsallcode.whiterabbit.api.model;
+/**
+ * The type of a {@link DayData day}.
+ */
public enum DayType
{
HOLIDAY(false), VACATION(false), FLEX_TIME(true), SICK(false), WORK(true), WEEKEND(false);
@@ -11,6 +14,9 @@ public enum DayType
this.workDay = workDay;
}
+ /**
+ * @return true
if you need to work on a day of this type.
+ */
public boolean isWorkDay()
{
return workDay;
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/MonthData.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/MonthData.java
new file mode 100644
index 00000000..af40d9e6
--- /dev/null
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/MonthData.java
@@ -0,0 +1,27 @@
+package org.itsallcode.whiterabbit.api.model;
+
+import java.time.Duration;
+import java.time.Month;
+import java.util.List;
+
+/**
+ * Model class for data of a single month containing year, month, day data etc.
+ */
+public interface MonthData
+{
+ int getYear();
+
+ void setYear(int year);
+
+ Month getMonth();
+
+ void setMonth(Month month);
+
+ Duration getOvertimePreviousMonth();
+
+ void setOvertimePreviousMonth(Duration overtimePreviousMonth);
+
+ List getDays();
+
+ void setDays(List days);
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/Project.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/Project.java
index 56c9d721..ae7d836e 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/model/Project.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/Project.java
@@ -1,5 +1,9 @@
package org.itsallcode.whiterabbit.api.model;
+/**
+ * The project you work on during an {@link ProjectReportActivity activity}
+ * containing an ID, label and cost carrier id.
+ */
public interface Project
{
String getProjectId();
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReport.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReport.java
index e4c4e2e5..92bebde8 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReport.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReport.java
@@ -3,6 +3,9 @@
import java.time.YearMonth;
import java.util.List;
+/**
+ * A monthly project report.
+ */
public interface ProjectReport
{
YearMonth getMonth();
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportActivity.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportActivity.java
index a7ecf8cc..787d1127 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportActivity.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportActivity.java
@@ -2,6 +2,10 @@
import java.time.Duration;
+/**
+ * An activity during a {@link ProjectReportDay day} where you worked for a
+ * specific time duration on a {@link Project}.
+ */
public interface ProjectReportActivity
{
Project getProject();
diff --git a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportDay.java b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportDay.java
index 3500d617..50dc8a60 100644
--- a/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportDay.java
+++ b/api/src/main/java/org/itsallcode/whiterabbit/api/model/ProjectReportDay.java
@@ -3,6 +3,10 @@
import java.time.LocalDate;
import java.util.List;
+/**
+ * The projects worked on during a single day of the {@link ProjectReport}
+ * including date and {@link ProjectReportActivity activities}.
+ */
public interface ProjectReportDay
{
LocalDate getDate();
diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/ProjectReportViewer.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/ProjectReportViewer.java
index 26cbbb8d..36d5a37c 100644
--- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/ProjectReportViewer.java
+++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/ProjectReportViewer.java
@@ -8,7 +8,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.itsallcode.whiterabbit.api.ProjectReportExporter;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
import org.itsallcode.whiterabbit.api.model.DayType;
import org.itsallcode.whiterabbit.api.model.ProjectReport;
import org.itsallcode.whiterabbit.api.model.ProjectReportActivity;
diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ProgressDialog.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ProgressDialog.java
index b054814a..b19d0d92 100644
--- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ProgressDialog.java
+++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ProgressDialog.java
@@ -2,7 +2,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.itsallcode.whiterabbit.api.ProgressMonitor;
+import org.itsallcode.whiterabbit.api.features.ProgressMonitor;
import org.itsallcode.whiterabbit.jfxui.JavaFxUtil;
import org.itsallcode.whiterabbit.jfxui.ui.UiResources;
diff --git a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/uistate/model/UiStateModelTest.java b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/uistate/model/UiStateModelTest.java
index 3ace3917..3c5a380c 100644
--- a/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/uistate/model/UiStateModelTest.java
+++ b/jfxui/src/test/java/org/itsallcode/whiterabbit/jfxui/uistate/model/UiStateModelTest.java
@@ -118,13 +118,11 @@ void deserializeSplitPaneState()
private UiStateModel deserialize(String json)
{
- final Jsonb jsonb = JsonbBuilder.create();
return jsonb.fromJson(json, UiStateModel.class);
}
private void assertSerialized(String expectedJson)
{
- final Jsonb jsonb = JsonbBuilder.create();
final String json = jsonb.toJson(model);
assertThat(json).isEqualTo(expectedJson);
}
diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTest.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTest.java
index 3b84d7a5..f40f1270 100644
--- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTest.java
+++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTest.java
@@ -11,9 +11,9 @@
import java.time.YearMonth;
import java.util.Locale;
+import org.itsallcode.whiterabbit.api.model.DayData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.jfxui.testutil.TestUtil;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testfx.api.FxRobot;
@@ -97,12 +97,12 @@ void jsonFileWrittenAfterMinuteTick()
time().tickMinute();
final LocalTime end = time().getCurrentTimeMinutes();
- final JsonMonth month = loadMonth(today);
+ final MonthData month = loadMonth(today);
assertAll(
() -> assertThat(month.getDays()).hasSize(1),
- () -> assertThat(month.getDays()).extracting(JsonDay::getBegin).containsExactly(begin),
- () -> assertThat(month.getDays()).extracting(JsonDay::getEnd).containsExactly(end));
+ () -> assertThat(month.getDays()).extracting(DayData::getBegin).containsExactly(begin),
+ () -> assertThat(month.getDays()).extracting(DayData::getEnd).containsExactly(end));
}
@Test
diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java
index 6234203c..ab8ce20f 100644
--- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java
+++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java
@@ -18,17 +18,17 @@
import java.util.Objects;
import javax.json.bind.Jsonb;
-import javax.json.bind.JsonbBuilder;
-import javax.json.bind.JsonbConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.jfxui.testutil.TableRowExpectedContent;
import org.itsallcode.whiterabbit.jfxui.testutil.TimeUtil;
import org.itsallcode.whiterabbit.jfxui.testutil.model.ApplicationHelper;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
-import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
import org.itsallcode.whiterabbit.logic.service.project.ProjectConfig;
+import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonMonth;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonbFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -44,7 +44,7 @@
abstract class JavaFxAppUiTestBase
{
private static final Logger LOG = LogManager.getLogger(JavaFxAppUiTestBase.class);
- private static final Jsonb JSONB = JsonbBuilder.create(new JsonbConfig().withFormatting(true));
+ private static final Jsonb JSONB = new JsonbFactory().create();
private JavaFxApp javaFxApp;
@@ -181,7 +181,7 @@ protected static ProjectImpl project(final String id, final String label)
return project;
}
- protected JsonMonth loadMonth(final LocalDate date)
+ protected MonthData loadMonth(final LocalDate date)
{
final Path file = this.dataDir.resolve(String.valueOf(date.getYear()))
.resolve(YearMonth.from(date).toString() + ".json");
diff --git a/logic/build.gradle b/logic/build.gradle
index def2d479..e4a035a5 100644
--- a/logic/build.gradle
+++ b/logic/build.gradle
@@ -6,7 +6,7 @@ dependencies {
api project(':api')
implementation "org.apache.logging.log4j:log4j-api:$log4jVersion"
implementation "javax.json.bind:javax.json.bind-api:${jsonBindApiVersion}"
- runtimeOnly "org.eclipse:yasson:${yassonVersion}"
+ implementation "org.eclipse:yasson:${yassonVersion}"
}
ext.generatedResourcesFolder = file("$buildDir/generated-resources")
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigLoader.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigLoader.java
index 02bcbe06..50f9325a 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigLoader.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/ConfigLoader.java
@@ -13,8 +13,8 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
import org.itsallcode.whiterabbit.logic.service.project.ProjectConfig;
+import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
public class ConfigLoader
{
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/Activity.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/Activity.java
index b25fd4b4..93b7620e 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/Activity.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/Activity.java
@@ -3,19 +3,19 @@
import java.time.Duration;
import java.util.Objects;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
import org.itsallcode.whiterabbit.api.model.Project;
-import org.itsallcode.whiterabbit.logic.model.json.JsonActivity;
import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
public class Activity implements RowRecord
{
- final JsonActivity jsonActivity;
+ final ActivityData jsonActivity;
private final DayActivities day;
private final int index;
private final ProjectService projectService;
- public Activity(int index, JsonActivity jsonActivity, DayActivities day, ProjectService projectService)
+ public Activity(int index, ActivityData jsonActivity, DayActivities day, ProjectService projectService)
{
this.index = index;
this.jsonActivity = jsonActivity;
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayActivities.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayActivities.java
index 57905e98..29200672 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayActivities.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayActivities.java
@@ -13,8 +13,9 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.itsallcode.whiterabbit.logic.model.json.JsonActivity;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
public class DayActivities
@@ -22,11 +23,13 @@ public class DayActivities
private static final Logger LOG = LogManager.getLogger(DayActivities.class);
private final ProjectService projectService;
- private final JsonDay day;
+ private final ModelFactory modelFactory;
+ private final DayData day;
final DayRecord dayRecord;
- DayActivities(DayRecord dayRecord, ProjectService projectService)
+ DayActivities(DayRecord dayRecord, ProjectService projectService, ModelFactory modelFactory)
{
+ this.modelFactory = modelFactory;
this.day = dayRecord.getJsonDay();
this.dayRecord = dayRecord;
this.projectService = Objects.requireNonNull(projectService);
@@ -40,7 +43,7 @@ public Activity add()
}
final boolean isFirstActivity = getActivities().findAny().isEmpty();
- final JsonActivity jsonActivity = new JsonActivity();
+ final ActivityData jsonActivity = modelFactory.createActivityData();
jsonActivity.setProjectId("");
jsonActivity.setDuration(Duration.ZERO);
final int newRowIndex = day.getActivities().size();
@@ -53,7 +56,7 @@ public Activity add()
public List getAll()
{
- final List jsonActivities = getActivities().collect(toList());
+ final List jsonActivities = getActivities().collect(toList());
return IntStream.range(0, jsonActivities.size())
.mapToObj(i -> wrapActivity(jsonActivities.get(i), i))
.collect(toList());
@@ -65,14 +68,14 @@ public boolean isEmpty()
.findFirst().orElse(true);
}
- private Stream getActivities()
+ private Stream getActivities()
{
return Optional.ofNullable(day.getActivities())
.orElse(emptyList())
.stream();
}
- private Activity wrapActivity(JsonActivity activity, int index)
+ private Activity wrapActivity(ActivityData activity, int index)
{
return new Activity(index, activity, this, projectService);
}
@@ -98,7 +101,7 @@ public void remove(int index)
}
}
- public void setRemainderActivity(JsonActivity activity, boolean remainder)
+ public void setRemainderActivity(ActivityData activity, boolean remainder)
{
if (activity.isRemainder() == remainder)
{
@@ -113,9 +116,9 @@ public void setRemainderActivity(JsonActivity activity, boolean remainder)
return;
}
- final List currentRemainders = getRemainderActivities();
+ final List currentRemainders = getRemainderActivities();
LOG.debug("Found {} remainder activities: deactivate them", currentRemainders.size());
- for (final JsonActivity remainderActivity : currentRemainders)
+ for (final ActivityData remainderActivity : currentRemainders)
{
final Duration unallocatedDuration = getUnallocatedDuration();
remainderActivity.setDuration(unallocatedDuration);
@@ -127,7 +130,7 @@ public void setRemainderActivity(JsonActivity activity, boolean remainder)
private Duration getUnallocatedDuration()
{
- final Duration allocatedDuration = getActivities().map(JsonActivity::getDuration)
+ final Duration allocatedDuration = getActivities().map(ActivityData::getDuration)
.filter(Objects::nonNull)
.reduce((d1, d2) -> d1.plus(d2))
.orElse(Duration.ZERO);
@@ -136,7 +139,7 @@ private Duration getUnallocatedDuration()
public boolean isValidAllocation()
{
- final List remainderActivities = getActivities()
+ final List remainderActivities = getActivities()
.filter(a -> a.getDuration() == null)
.collect(toList());
if (remainderActivities.size() > 1)
@@ -159,7 +162,7 @@ public boolean isValidAllocation()
return true;
}
- private List getRemainderActivities()
+ private List getRemainderActivities()
{
return getActivities().filter(a -> a.getDuration() == null).collect(toList());
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayRecord.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayRecord.java
index 29bd7c6d..42dbc350 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayRecord.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/DayRecord.java
@@ -8,8 +8,9 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.api.model.DayType;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
@@ -19,18 +20,20 @@ public class DayRecord implements RowRecord
private final ContractTermsService contractTerms;
private final ProjectService projectService;
- private final JsonDay day;
+ private final ModelFactory modelFactory;
+ private final DayData day;
private final MonthIndex month;
private final DayRecord previousDay;
- public DayRecord(ContractTermsService contractTerms, JsonDay day, DayRecord previousDay, MonthIndex month,
- ProjectService projectService)
+ public DayRecord(ContractTermsService contractTerms, DayData day, DayRecord previousDay, MonthIndex month,
+ ProjectService projectService, ModelFactory modelFactory)
{
this.contractTerms = contractTerms;
+ this.projectService = Objects.requireNonNull(projectService);
+ this.modelFactory = modelFactory;
this.day = day;
this.previousDay = previousDay;
this.month = month;
- this.projectService = Objects.requireNonNull(projectService);
}
public Duration getMandatoryBreak()
@@ -148,7 +151,7 @@ public void setInterruption(Duration interruption)
day.setInterruption(interruption.isZero() ? null : interruption);
}
- JsonDay getJsonDay()
+ DayData getJsonDay()
{
return day;
}
@@ -184,7 +187,7 @@ public boolean isDummyDay()
public DayActivities activities()
{
- return new DayActivities(this, projectService);
+ return new DayActivities(this, projectService, modelFactory);
}
@Override
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/MonthIndex.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/MonthIndex.java
index 1f35581d..c8de5a8a 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/MonthIndex.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/MonthIndex.java
@@ -10,33 +10,38 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.api.model.DayType;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
public class MonthIndex
{
- private final JsonMonth record;
+ private final ModelFactory modelFactory;
+ private final MonthData record;
private final Map days;
- private MonthIndex(JsonMonth record, Map days)
+ private MonthIndex(ModelFactory modelFactory, MonthData record, Map days)
{
+ this.modelFactory = Objects.requireNonNull(modelFactory, "modelFactory");
this.record = record;
this.days = days;
}
- public static MonthIndex create(ContractTermsService contractTerms, JsonMonth record, ProjectService projectService)
+ public static MonthIndex create(ContractTermsService contractTerms, ProjectService projectService,
+ ModelFactory modelFactory, MonthData record)
{
- final Map jsonDays = record.getDays().stream()
- .collect(toMap(JsonDay::getDate, Function.identity()));
+ final Map jsonDays = record.getDays().stream()
+ .collect(toMap(DayData::getDate, Function.identity()));
final Map days = new HashMap<>();
- final MonthIndex monthIndex = new MonthIndex(record, days);
+ final MonthIndex monthIndex = new MonthIndex(modelFactory, record, days);
final YearMonth yearMonth = YearMonth.of(record.getYear(), record.getMonth());
@@ -44,8 +49,9 @@ public static MonthIndex create(ContractTermsService contractTerms, JsonMonth re
for (int day = 1; day <= yearMonth.lengthOfMonth(); day++)
{
final LocalDate date = yearMonth.atDay(day);
- final JsonDay jsonDay = jsonDays.computeIfAbsent(date, d -> createDummyDay(d, contractTerms));
- final DayRecord dayRecord = new DayRecord(contractTerms, jsonDay, previousDay, monthIndex, projectService);
+ final DayData jsonDay = jsonDays.computeIfAbsent(date, d -> createDummyDay(d, contractTerms, modelFactory));
+ final DayRecord dayRecord = new DayRecord(contractTerms, jsonDay, previousDay, monthIndex, projectService,
+ modelFactory);
days.put(dayRecord.getDate(), dayRecord);
previousDay = dayRecord;
}
@@ -53,9 +59,9 @@ public static MonthIndex create(ContractTermsService contractTerms, JsonMonth re
return monthIndex;
}
- private static JsonDay createDummyDay(LocalDate date, ContractTermsService contractTerms)
+ private static DayData createDummyDay(LocalDate date, ContractTermsService contractTerms, ModelFactory modelFactory)
{
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(date);
if (contractTerms.getContractedWorkingTimePerDay() != null
&& !contractTerms.getContractedWorkingTimePerDay().equals(contractTerms.getCurrentWorkingTimePerDay()))
@@ -80,13 +86,19 @@ public void put(DayRecord day)
this.days.put(day.getDate(), day);
}
- public JsonMonth getMonthRecord()
+ public MonthData getMonthRecord()
{
- final List sortedNonDummyJsonDays = getSortedDays() //
+ final List sortedNonDummyJsonDays = getSortedDays() //
.filter(d -> !d.isDummyDay()) //
.map(DayRecord::getJsonDay) //
.collect(toList());
- return JsonMonth.create(record, sortedNonDummyJsonDays);
+
+ final MonthData month = modelFactory.createMonthData();
+ month.setOvertimePreviousMonth(record.getOvertimePreviousMonth());
+ month.setYear(record.getYear());
+ month.setMonth(record.getMonth());
+ month.setDays(sortedNonDummyJsonDays);
+ return month;
}
public Duration getOvertimePreviousMonth()
@@ -126,7 +138,7 @@ public List getVacationDays()
{
return record.getDays().stream()
.filter(day -> day.getType() == DayType.VACATION)
- .map(JsonDay::getDate)
+ .map(DayData::getDate)
.collect(toList());
}
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGenerator.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGenerator.java
index 91aa8ad9..eb0ee679 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGenerator.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGenerator.java
@@ -43,7 +43,7 @@ public VacationReport generateReport()
private class Calculator
{
- final List availableDataYearMonth = storage.getAvailableDataYearMonth();
+ final List availableDataYearMonth = storage.getAvailableDataMonths();
final Map monthData = availableDataYearMonth.stream() //
.map(storage::loadMonth) //
.map(Optional::orElseThrow) //
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/AppService.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/AppService.java
index 3bcaa2fd..da70bcc1 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/AppService.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/AppService.java
@@ -1,6 +1,5 @@
package org.itsallcode.whiterabbit.logic.service;
-import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import java.io.Closeable;
@@ -17,6 +16,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
import org.itsallcode.whiterabbit.api.model.ProjectReport;
import org.itsallcode.whiterabbit.logic.Config;
import org.itsallcode.whiterabbit.logic.autocomplete.AutocompleteService;
@@ -37,6 +37,7 @@
import org.itsallcode.whiterabbit.logic.service.singleinstance.SingleInstanceService;
import org.itsallcode.whiterabbit.logic.storage.CachingStorage;
import org.itsallcode.whiterabbit.logic.storage.Storage;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonFileStorage;
public class AppService implements Closeable
{
@@ -94,8 +95,10 @@ public static AppService create(final Config config, Clock clock, ScheduledExecu
{
final SingleInstanceService singleInstanceService = SingleInstanceService.create(config);
final ProjectService projectService = new ProjectService(config);
+ final PluginManager pluginManager = PluginManager.create(config);
- final CachingStorage storage = CachingStorage.create(config.getDataDir(), new ContractTermsService(config),
+ final MonthDataStorage dataStorage = createMonthDataStorage(config, pluginManager);
+ final CachingStorage storage = CachingStorage.create(dataStorage, new ContractTermsService(config),
projectService);
final ClockService clockService = new ClockService(clock);
final AutocompleteService autocompleteService = new AutocompleteService(storage, clockService);
@@ -106,13 +109,24 @@ public static AppService create(final Config config, Clock clock, ScheduledExecu
final ProjectReportGenerator projectReportGenerator = new ProjectReportGenerator(storage);
final ActivityService activityService = new ActivityService(storage, autocompleteService, appServiceCallback);
final FormatterService formatterService = new FormatterService(config.getLocale(), clock.getZone());
- final PluginManager pluginManager = PluginManager.create(config);
return new AppService(workingTimeService, storage, formatterService, clockService, schedulingService,
singleInstanceService, appServiceCallback, activityService, projectService, autocompleteService,
new AppPropertiesService(), vacationReportGenerator,
projectReportGenerator, pluginManager);
}
+ private static MonthDataStorage createMonthDataStorage(final Config config, final PluginManager pluginManager)
+ {
+ final Optional dataStorage = pluginManager.getMonthDataStorage();
+ if (dataStorage.isPresent())
+ {
+ LOG.info("Using storage plugin {}", dataStorage.get().getClass().getName());
+ return dataStorage.get();
+ }
+ LOG.debug("No storage plugin found, using default json file storage");
+ return JsonFileStorage.create(config.getDataDir());
+ }
+
public void setUpdateListener(AppServiceCallback callback)
{
this.appServiceCallback.setDelegate(callback);
@@ -155,6 +169,10 @@ public SchedulingService scheduler()
return this.schedulingService;
}
+ /**
+ * @deprecated will be removed in the next version.
+ */
+ @Deprecated
public void report()
{
final DayReporter reporter = new DayReporter(formatterService);
@@ -182,21 +200,9 @@ public ClockService getClock()
return clock;
}
- public List getRecords(YearMonth yearMonth)
- {
- return getMonth(yearMonth) //
- .map(record -> record.getSortedDays().collect(toList())) //
- .orElse(emptyList());
- }
-
public List getAvailableDataYearMonth()
{
- return storage.getAvailableDataYearMonth();
- }
-
- public Optional getMonth(YearMonth yearMonth)
- {
- return storage.loadMonth(yearMonth);
+ return storage.getAvailableDataMonths();
}
public MonthIndex getOrCreateMonth(YearMonth yearMonth)
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/DayReporter.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/DayReporter.java
index 0649e0d6..ce8f63bc 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/DayReporter.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/DayReporter.java
@@ -6,6 +6,10 @@
import org.apache.logging.log4j.Logger;
import org.itsallcode.whiterabbit.logic.model.DayRecord;
+/**
+ * @deprecated will be removed in the next version.
+ */
+@Deprecated
public class DayReporter
{
private static final Logger LOG = LogManager.getLogger(DayReporter.class);
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/FormatterService.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/FormatterService.java
index 585f30ee..358c443d 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/FormatterService.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/FormatterService.java
@@ -30,6 +30,10 @@ public FormatterService(Locale locale, ZoneId timeZoneId)
.withLocale(locale).withZone(timeZoneId);
}
+ /**
+ * @deprecated will be removed in the next version.
+ */
+ @Deprecated
public String format(DayRecord day)
{
final String dayOfWeek = day.getDate().getDayOfWeek().getDisplayName(TextStyle.SHORT_STANDALONE, locale);
@@ -44,6 +48,10 @@ public String format(DayRecord day)
format(day.getOverallOvertime()));
}
+ /**
+ * @deprecated will be removed in the next version.
+ */
+ @Deprecated
private String formatDayType(DayType type)
{
final String formatPattern = "%1$-" + MAX_DAY_TYPE_LENGTH + "s";
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManager.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManager.java
index ba03bda6..f04f0ab6 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManager.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManager.java
@@ -3,10 +3,13 @@
import static java.util.stream.Collectors.toList;
import java.util.List;
+import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.itsallcode.whiterabbit.api.ProjectReportExporter;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
import org.itsallcode.whiterabbit.logic.Config;
public class PluginManager
@@ -32,7 +35,7 @@ public List getProjectReportExporterPlugins()
return findPluginsSupporting(ProjectReportExporter.class);
}
- private List findPluginsSupporting(Class> featureType)
+ private List findPluginsSupporting(Class extends PluginFeature> featureType)
{
return pluginRegistry.getAllPlugins().stream()
.filter(plugin -> plugin.supports(featureType))
@@ -45,9 +48,38 @@ public ProjectReportExporter getProjectReportExporter(String id)
return getFeature(id, ProjectReportExporter.class);
}
- private T getFeature(String id, final Class featureType)
+ public Optional getMonthDataStorage()
{
- return pluginRegistry.getPlugin(id).getFeature(featureType);
+ return getUniqueFeature(MonthDataStorage.class);
+ }
+
+ private Optional getUniqueFeature(Class featureType)
+ {
+ final List pluginIds = findPluginsSupporting(featureType);
+ if (pluginIds.isEmpty())
+ {
+ return Optional.empty();
+ }
+ if (pluginIds.size() > 1)
+ {
+ throw new IllegalStateException("Found multiple plugins supporting " + featureType.getName()
+ + ": " + pluginIds + ". Please add only one storage plugin to the classpath.");
+ }
+ return Optional.of(getFeature(pluginIds.get(0), featureType));
+ }
+
+ private T getFeature(String id, final Class featureType)
+ {
+ final PluginWrapper plugin = pluginRegistry.getPlugin(id);
+ if (plugin == null)
+ {
+ throw new IllegalStateException("Plugin '" + id + "' not found");
+ }
+ if (!plugin.supports(featureType))
+ {
+ throw new IllegalStateException("Plugin '" + id + "' does not support feature " + featureType.getName());
+ }
+ return plugin.getFeature(featureType);
}
public void close()
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistry.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistry.java
index ec0af087..f34d7806 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistry.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistry.java
@@ -95,7 +95,7 @@ private ClassLoader createClassLoader(Path jar)
private List getPluginJars()
{
final Path pluginDir = config.getPluginDir();
- if (!Files.exists(pluginDir))
+ if (pluginDir == null || !Files.exists(pluginDir))
{
LOG.info("Plugin directory {} does not exist", pluginDir);
return emptyList();
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginWrapper.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginWrapper.java
index 4b47d789..a1a002cb 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginWrapper.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginWrapper.java
@@ -4,6 +4,7 @@
import org.itsallcode.whiterabbit.api.Plugin;
import org.itsallcode.whiterabbit.api.PluginConfiguration;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
import org.itsallcode.whiterabbit.logic.Config;
class PluginWrapper
@@ -34,12 +35,12 @@ String getId()
return plugin.getId();
}
- boolean supports(Class> featureType)
+ boolean supports(Class extends PluginFeature> featureType)
{
return plugin.supports(featureType);
}
- T getFeature(Class featureType)
+ T getFeature(Class featureType)
{
return plugin.getFeature(featureType);
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/project/ProjectService.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/project/ProjectService.java
index d5ed6041..cda8d919 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/project/ProjectService.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/service/project/ProjectService.java
@@ -41,7 +41,7 @@ public ProjectService(Config config)
this.jsonb = jsonb;
final Path projectConfigFile = config.getProjectFile();
- if (Files.exists(projectConfigFile))
+ if (projectConfigFile != null && Files.exists(projectConfigFile))
{
final List allProjects = loadAvailableProjects(projectConfigFile);
projectsById = groupBy(allProjects, ProjectImpl::getProjectId);
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorage.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorage.java
index 41b7b245..61382120 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorage.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorage.java
@@ -1,24 +1,19 @@
package org.itsallcode.whiterabbit.logic.storage;
+import java.time.LocalDate;
+import java.util.List;
+
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
import org.itsallcode.whiterabbit.logic.model.DayRecord;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
-import javax.json.bind.Jsonb;
-import javax.json.bind.JsonbBuilder;
-import javax.json.bind.JsonbConfig;
-import java.nio.file.Path;
-import java.time.LocalDate;
-import java.util.List;
-
public interface CachingStorage extends Storage
{
- static CachingStorage create(Path dataDir, ContractTermsService contractTerms, ProjectService projectService)
+ static CachingStorage create(MonthDataStorage dataStorage, ContractTermsService contractTerms,
+ ProjectService projectService)
{
- final DateToFileMapper dateToFileMapper = new DateToFileMapper(dataDir);
- final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true));
- final JsonFileStorage fileStorage = new JsonFileStorage(jsonb, dateToFileMapper);
- final MonthIndexStorage monthIndexStorage = new MonthIndexStorage(contractTerms, projectService, fileStorage);
+ final MonthIndexStorage monthIndexStorage = new MonthIndexStorage(contractTerms, projectService, dataStorage);
return new CachingStorageImpl(monthIndexStorage);
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImpl.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImpl.java
index c2d2f8f8..114e18cb 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImpl.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImpl.java
@@ -68,9 +68,9 @@ private MonthIndex updateCache(MonthIndex month)
}
@Override
- public List getAvailableDataYearMonth()
+ public List getAvailableDataMonths()
{
- return delegateStorage.getAvailableDataYearMonth();
+ return delegateStorage.getAvailableDataMonths();
}
@Override
@@ -95,7 +95,7 @@ void ensureLatestDaysCached(LocalDate maxAge)
List getRequiredYearMonths(LocalDate maxAge)
{
final YearMonth oldestYearMonth = YearMonth.from(maxAge);
- return delegateStorage.getAvailableDataYearMonth().stream()
+ return delegateStorage.getAvailableDataMonths().stream()
.filter(month -> !month.isBefore(oldestYearMonth))
.collect(toList());
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorage.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorage.java
index 37687b84..8416517f 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorage.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorage.java
@@ -11,9 +11,10 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.model.MonthIndex;
import org.itsallcode.whiterabbit.logic.model.MultiMonthIndex;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
@@ -23,10 +24,10 @@ class MonthIndexStorage implements Storage
private final ContractTermsService contractTerms;
private final ProjectService projectService;
- private final JsonFileStorage fileStorage;
+ private final MonthDataStorage fileStorage;
MonthIndexStorage(ContractTermsService contractTerms, ProjectService projectService,
- JsonFileStorage fileStorage)
+ MonthDataStorage fileStorage)
{
this.contractTerms = contractTerms;
this.projectService = projectService;
@@ -36,7 +37,7 @@ class MonthIndexStorage implements Storage
@Override
public Optional loadMonth(YearMonth date)
{
- return fileStorage.loadMonthRecord(date).map(this::createMonthIndex);
+ return fileStorage.load(date).map(this::createMonthIndex);
}
@Override
@@ -49,7 +50,7 @@ public MonthIndex loadOrCreate(final YearMonth yearMonth)
@Override
public void storeMonth(MonthIndex month)
{
- fileStorage.writeToFile(month.getYearMonth(), month.getMonthRecord());
+ fileStorage.store(month.getYearMonth(), month.getMonthRecord());
}
@Override
@@ -61,14 +62,14 @@ public MultiMonthIndex loadAll()
}
@Override
- public List getAvailableDataYearMonth()
+ public List getAvailableDataMonths()
{
- return fileStorage.getAvailableDataYearMonth();
+ return fileStorage.getAvailableDataMonths();
}
private MonthIndex createNewMonth(YearMonth date)
{
- final JsonMonth month = new JsonMonth();
+ final MonthData month = fileStorage.getModelFactory().createMonthData();
month.setYear(date.getYear());
month.setMonth(date.getMonth());
month.setDays(new ArrayList<>());
@@ -76,9 +77,9 @@ private MonthIndex createNewMonth(YearMonth date)
return createMonthIndex(month);
}
- private MonthIndex createMonthIndex(final JsonMonth jsonMonth)
+ private MonthIndex createMonthIndex(final MonthData jsonMonth)
{
- return MonthIndex.create(contractTerms, jsonMonth, projectService);
+ return MonthIndex.create(contractTerms, projectService, fileStorage.getModelFactory(), jsonMonth);
}
Duration loadPreviousMonthOvertime(YearMonth date)
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/Storage.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/Storage.java
index 011ad687..b1f1af50 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/Storage.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/Storage.java
@@ -17,6 +17,6 @@ public interface Storage
MultiMonthIndex loadAll();
- List getAvailableDataYearMonth();
+ List getAvailableDataMonths();
}
\ No newline at end of file
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapper.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapper.java
similarity index 96%
rename from logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapper.java
rename to logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapper.java
index 663e4c65..f130ba51 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapper.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapper.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.storage;
+package org.itsallcode.whiterabbit.logic.storage.data;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -15,7 +15,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-public class DateToFileMapper
+class DateToFileMapper
{
private static final Logger LOG = LogManager.getLogger(DateToFileMapper.class);
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonActivity.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonActivity.java
similarity index 78%
rename from logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonActivity.java
rename to logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonActivity.java
index 7ed8f794..07cb58b2 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonActivity.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonActivity.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.model.json;
+package org.itsallcode.whiterabbit.logic.storage.data;
import java.time.Duration;
@@ -6,9 +6,12 @@
import javax.json.bind.annotation.JsonbPropertyOrder;
import javax.json.bind.annotation.JsonbVisibility;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.logic.model.json.FieldAccessStrategy;
+
@JsonbPropertyOrder({ "projectId", "duration", "comment" })
@JsonbVisibility(FieldAccessStrategy.class)
-public class JsonActivity
+public class JsonActivity implements ActivityData
{
@JsonbProperty("projectId")
private String projectId;
@@ -17,43 +20,43 @@ public class JsonActivity
@JsonbProperty("comment")
private String comment;
- public JsonActivity()
- {
- this.projectId = null;
- this.duration = null;
- this.comment = null;
- }
-
+ @Override
public String getProjectId()
{
return projectId;
}
+ @Override
public void setProjectId(String id)
{
this.projectId = id;
}
+ @Override
public Duration getDuration()
{
return duration;
}
+ @Override
public boolean isRemainder()
{
return getDuration() == null;
}
+ @Override
public void setDuration(Duration duration)
{
this.duration = duration;
}
+ @Override
public String getComment()
{
return comment;
}
+ @Override
public void setComment(String comment)
{
this.comment = comment;
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonDay.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonDay.java
similarity index 80%
rename from logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonDay.java
rename to logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonDay.java
index a1be07af..4eadc4c6 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonDay.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonDay.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.model.json;
+package org.itsallcode.whiterabbit.logic.storage.data;
import java.time.Duration;
import java.time.LocalDate;
@@ -8,10 +8,12 @@
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbPropertyOrder;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.api.model.DayType;
@JsonbPropertyOrder({ "date", "type", "begin", "end", "interruption", "workingHours", "comment", "activities" })
-public class JsonDay
+public class JsonDay implements DayData
{
@JsonbProperty("date")
private LocalDate date;
@@ -28,84 +30,100 @@ public class JsonDay
@JsonbProperty("comment")
private String comment;
@JsonbProperty("activities")
- private List activities;
+ private List activities;
+ @Override
public LocalDate getDate()
{
return date;
}
+ @Override
public DayType getType()
{
return type;
}
+ @Override
public LocalTime getBegin()
{
return begin;
}
+ @Override
public LocalTime getEnd()
{
return end;
}
+ @Override
public Duration getInterruption()
{
return interruption;
}
+ @Override
public Duration getWorkingHours()
{
return workingHours;
}
+ @Override
public String getComment()
{
return comment;
}
+ @Override
public void setDate(LocalDate date)
{
this.date = date;
}
+ @Override
public void setType(DayType type)
{
this.type = type;
}
+ @Override
public void setBegin(LocalTime begin)
{
this.begin = begin;
}
+ @Override
public void setEnd(LocalTime end)
{
this.end = end;
}
+ @Override
public void setWorkingHours(Duration workingHours)
{
this.workingHours = workingHours;
}
+ @Override
public void setComment(String comment)
{
this.comment = comment;
}
+ @Override
public void setInterruption(Duration interruption)
{
this.interruption = interruption;
}
- public List getActivities()
+ @Override
+ public List getActivities()
{
return activities;
}
- public void setActivities(List activities)
+ @Override
+ public void setActivities(List activities)
{
this.activities = activities;
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorage.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorage.java
similarity index 77%
rename from logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorage.java
rename to logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorage.java
index aed94d00..f97dcb9f 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorage.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorage.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.storage;
+package org.itsallcode.whiterabbit.logic.storage.data;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
@@ -18,36 +18,33 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.Config;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
-public class JsonFileStorage
+public class JsonFileStorage implements MonthDataStorage
{
private static final Logger LOG = LogManager.getLogger(JsonFileStorage.class);
private final Jsonb jsonb;
private final DateToFileMapper dateToFileMapper;
+ private final ModelFactory modelFactory;
- JsonFileStorage(Jsonb jsonb, DateToFileMapper dateToFileMapper)
+ JsonFileStorage(Jsonb jsonb, DateToFileMapper dateToFileMapper, ModelFactory modelFactory)
{
this.jsonb = jsonb;
this.dateToFileMapper = dateToFileMapper;
+ this.modelFactory = modelFactory;
}
- private JsonMonth loadFromFile(Path file)
+ public static MonthDataStorage create(Path dataDir)
{
- LOG.trace("Reading file {}", file);
- try (InputStream stream = Files.newInputStream(file))
- {
- return jsonb.fromJson(stream, JsonMonth.class);
- }
- catch (final IOException e)
- {
- throw new UncheckedIOException("Error reading file " + file, e);
- }
+ final Jsonb jsonb = new JsonbFactory().createNonFormatting();
+ return new JsonFileStorage(jsonb, new DateToFileMapper(dataDir), new JsonModelFactory());
}
- Optional loadMonthRecord(YearMonth date)
+ @Override
+ public Optional load(YearMonth date)
{
final Path file = dateToFileMapper.getPathForDate(date);
if (file.toFile().exists())
@@ -65,7 +62,21 @@ Optional loadMonthRecord(YearMonth date)
return Optional.empty();
}
- void writeToFile(YearMonth yearMonth, JsonMonth record)
+ private JsonMonth loadFromFile(Path file)
+ {
+ LOG.trace("Reading file {}", file);
+ try (InputStream stream = Files.newInputStream(file))
+ {
+ return jsonb.fromJson(stream, JsonMonth.class);
+ }
+ catch (final IOException e)
+ {
+ throw new UncheckedIOException("Error reading file " + file, e);
+ }
+ }
+
+ @Override
+ public void store(YearMonth yearMonth, MonthData record)
{
final Path file = dateToFileMapper.getPathForDate(yearMonth);
LOG.trace("Write month {} to file {}", yearMonth, file);
@@ -97,12 +108,14 @@ private void createDirectory(Path dir)
}
}
- List getAvailableDataYearMonth()
+ @Override
+ public List getAvailableDataMonths()
{
return dateToFileMapper.getAllYearMonths().sorted().collect(toList());
}
- List loadAll()
+ @Override
+ public List loadAll()
{
return dateToFileMapper.getAllFiles()
.filter(file -> !file.getFileName().toString().equals(Config.PROJECTS_JSON))
@@ -110,4 +123,10 @@ List loadAll()
.sorted(comparing(JsonMonth::getYear).thenComparing(JsonMonth::getMonth))
.collect(toList());
}
+
+ @Override
+ public ModelFactory getModelFactory()
+ {
+ return modelFactory;
+ }
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonModelFactory.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonModelFactory.java
new file mode 100644
index 00000000..c6418b3e
--- /dev/null
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonModelFactory.java
@@ -0,0 +1,27 @@
+package org.itsallcode.whiterabbit.logic.storage.data;
+
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
+
+public class JsonModelFactory implements ModelFactory
+{
+ @Override
+ public MonthData createMonthData()
+ {
+ return new JsonMonth();
+ }
+
+ @Override
+ public DayData createDayData()
+ {
+ return new JsonDay();
+ }
+
+ @Override
+ public ActivityData createActivityData()
+ {
+ return new JsonActivity();
+ }
+}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonMonth.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonMonth.java
similarity index 66%
rename from logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonMonth.java
rename to logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonMonth.java
index 521de128..ba204c78 100644
--- a/logic/src/main/java/org/itsallcode/whiterabbit/logic/model/json/JsonMonth.java
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonMonth.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.model.json;
+package org.itsallcode.whiterabbit.logic.storage.data;
import java.time.Duration;
import java.time.Month;
@@ -7,8 +7,11 @@
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbPropertyOrder;
+import org.itsallcode.whiterabbit.api.model.DayData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
+
@JsonbPropertyOrder({ "year", "month", "overtimePreviousMonth", "days" })
-public class JsonMonth
+public class JsonMonth implements MonthData
{
@JsonbProperty("year")
private int year;
@@ -17,54 +20,52 @@ public class JsonMonth
@JsonbProperty("overtimePreviousMonth")
private Duration overtimePreviousMonth;
@JsonbProperty("days")
- private List days;
-
- public static JsonMonth create(JsonMonth record, List sortedDays)
- {
- final JsonMonth month = new JsonMonth();
- month.setOvertimePreviousMonth(record.getOvertimePreviousMonth());
- month.setYear(record.getYear());
- month.setMonth(record.getMonth());
- month.setDays(sortedDays);
- return month;
- }
+ private List days;
+ @Override
public int getYear()
{
return year;
}
+ @Override
public void setYear(int year)
{
this.year = year;
}
+ @Override
public Month getMonth()
{
return month;
}
+ @Override
public void setMonth(Month month)
{
this.month = month;
}
+ @Override
public Duration getOvertimePreviousMonth()
{
return overtimePreviousMonth;
}
+ @Override
public void setOvertimePreviousMonth(Duration overtimePreviousMonth)
{
this.overtimePreviousMonth = overtimePreviousMonth;
}
- public List getDays()
+ @Override
+ public List getDays()
{
return days;
}
- public void setDays(List days)
+ @Override
+ public void setDays(List days)
{
this.days = days;
}
diff --git a/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonbFactory.java b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonbFactory.java
new file mode 100644
index 00000000..263912d1
--- /dev/null
+++ b/logic/src/main/java/org/itsallcode/whiterabbit/logic/storage/data/JsonbFactory.java
@@ -0,0 +1,35 @@
+package org.itsallcode.whiterabbit.logic.storage.data;
+
+import java.util.Map;
+
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+
+import org.eclipse.yasson.YassonConfig;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
+
+public class JsonbFactory
+{
+ public Jsonb create()
+ {
+ return createWithFormatting(true);
+ }
+
+ public Jsonb createNonFormatting()
+ {
+ return createWithFormatting(false);
+ }
+
+ private Jsonb createWithFormatting(boolean formatting)
+ {
+ final Map, Class>> userTypeMapping = Map.of(
+ DayData.class, JsonDay.class,
+ ActivityData.class, JsonActivity.class);
+ final JsonbConfig config = new JsonbConfig()
+ .withFormatting(formatting)
+ .setProperty(YassonConfig.USER_TYPE_MAPPING, userTypeMapping);
+ return JsonbBuilder.create(config);
+ }
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayActivitiesTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayActivitiesTest.java
index 298ceb6e..0181918c 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayActivitiesTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayActivitiesTest.java
@@ -8,10 +8,12 @@
import java.time.Duration;
import java.util.ArrayList;
-import org.itsallcode.whiterabbit.logic.model.json.JsonActivity;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,13 +27,15 @@ class DayActivitiesTest
private ProjectService projectServiceMock;
@Mock
private ContractTermsService contractTermsMock;
- private JsonDay jsonDay;
+ private ModelFactory modelFactory;
+ private DayData jsonDay;
private DayRecord dayRecord;
@BeforeEach
void setup()
{
- jsonDay = new JsonDay();
+ modelFactory = new JsonModelFactory();
+ jsonDay = modelFactory.createDayData();
}
@Test
@@ -282,7 +286,7 @@ void setRemainderToFalse()
when(contractTermsMock.getWorkingTime(same(dayRecord))).thenReturn(Duration.ofHours(7));
- final JsonActivity activity = new JsonActivity();
+ final ActivityData activity = modelFactory.createActivityData();
activity.setDuration(null);
activities.setRemainderActivity(activity, false);
@@ -299,7 +303,7 @@ void setRemainderToFalseSetsRemainingDuration()
when(contractTermsMock.getWorkingTime(same(dayRecord))).thenReturn(Duration.ofHours(7));
activities.add().setDuration(Duration.ofHours(3));
- final JsonActivity activity = new JsonActivity();
+ final ActivityData activity = modelFactory.createActivityData();
activity.setDuration(null);
activities.setRemainderActivity(activity, false);
@@ -317,7 +321,7 @@ void setRemainderToFalseSetsRemainingDurationMultipleActivities()
activities.add().setDuration(Duration.ofHours(3));
activities.add().setDuration(Duration.ofMinutes(30));
- final JsonActivity activity = new JsonActivity();
+ final ActivityData activity = modelFactory.createActivityData();
activity.setDuration(null);
activities.setRemainderActivity(activity, false);
@@ -330,7 +334,7 @@ private DayActivities create()
{
final DayRecord previousDay = null;
final MonthIndex month = null;
- dayRecord = new DayRecord(contractTermsMock, jsonDay, previousDay, month, projectServiceMock);
- return new DayActivities(dayRecord, projectServiceMock);
+ dayRecord = new DayRecord(contractTermsMock, jsonDay, previousDay, month, projectServiceMock, modelFactory);
+ return new DayActivities(dayRecord, projectServiceMock, modelFactory);
}
}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java
index c55a74cf..afda7931 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/DayRecordTest.java
@@ -8,12 +8,14 @@
import java.time.LocalDate;
import java.time.LocalTime;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.api.model.DayType;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.Config;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.itsallcode.whiterabbit.logic.test.TestingConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,6 +27,7 @@ class DayRecordTest
{
@Mock
private ProjectService projectServiceMock;
+ private final ModelFactory modelFactory = new JsonModelFactory();
@Test
void mandatoryWorkingTimeIsZeroOnWeekend()
@@ -323,7 +326,7 @@ void setInterruptionFailsForNullValue()
@Test
void setInterruptionToZeroUsesNullForJsonRecord()
{
- final JsonDay day1 = new JsonDay();
+ final DayData day1 = modelFactory.createDayData();
day1.setInterruption(Duration.ofHours(1));
final DayRecord day = dayRecord(day1, null, null);
@@ -336,7 +339,7 @@ void setInterruptionToZeroUsesNullForJsonRecord()
@Test
void setCommentToEmptyStringUsesNullForJsonRecord()
{
- final JsonDay day1 = new JsonDay();
+ final DayData day1 = modelFactory.createDayData();
day1.setComment("comment");
final DayRecord day = dayRecord(day1, null, null);
@@ -349,7 +352,7 @@ void setCommentToEmptyStringUsesNullForJsonRecord()
@Test
void setCommentToValueSetsCommentForJsonRecord()
{
- final JsonDay day1 = new JsonDay();
+ final DayData day1 = modelFactory.createDayData();
final DayRecord day = dayRecord(day1, null, null);
day.setComment("comment");
@@ -399,41 +402,41 @@ void getMonthReturnsMonth()
}
@Test
- void getJsonDayReturnsMonth()
+ void getDayDataReturnsMonth()
{
- final JsonDay jsonDay = new JsonDay();
- final DayRecord day = createDay(jsonDay);
- assertThat(day.getJsonDay()).isSameAs(jsonDay);
+ final DayData dayData = modelFactory.createDayData();
+ final DayRecord day = createDay(dayData);
+ assertThat(day.getJsonDay()).isSameAs(dayData);
}
@Test
void getCustomWorkingTimeReturnsEmptyOptionalByDefault()
{
- final JsonDay jsonDay = new JsonDay();
- jsonDay.setWorkingHours(null);
+ final DayData DayData = modelFactory.createDayData();
+ DayData.setWorkingHours(null);
- final DayRecord day = createDay(jsonDay);
+ final DayRecord day = createDay(DayData);
assertThat(day.getCustomWorkingTime()).isEmpty();
}
@Test
void getCustomWorkingTimeReturnsRealValue()
{
- final JsonDay jsonDay = new JsonDay();
- jsonDay.setWorkingHours(Duration.ofHours(5));
+ final DayData DayData = modelFactory.createDayData();
+ DayData.setWorkingHours(Duration.ofHours(5));
- final DayRecord day = createDay(jsonDay);
+ final DayRecord day = createDay(DayData);
assertThat(day.getCustomWorkingTime()).isPresent().contains(Duration.ofHours(5));
}
- private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, JsonDay... days)
+ private MonthIndex month(LocalDate date, Duration overtimePreviousMonth, DayData... days)
{
- final JsonMonth jsonMonth = new JsonMonth();
+ final MonthData jsonMonth = modelFactory.createMonthData();
jsonMonth.setDays(asList(days));
jsonMonth.setMonth(date.getMonth());
jsonMonth.setYear(date.getYear());
jsonMonth.setOvertimePreviousMonth(overtimePreviousMonth);
- return MonthIndex.create(contractTerms(), jsonMonth, projectServiceMock);
+ return MonthIndex.create(contractTerms(), projectServiceMock, modelFactory, jsonMonth);
}
private void assertOvertime(LocalDate date, LocalTime begin, LocalTime end, Duration expectedOvertime)
@@ -517,7 +520,7 @@ private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayT
private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayType type, Duration interruption,
DayRecord previousDay, MonthIndex month)
{
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setBegin(begin);
day.setEnd(end);
day.setDate(date);
@@ -527,15 +530,15 @@ private DayRecord createDay(LocalDate date, LocalTime begin, LocalTime end, DayT
return dayRecord(day, previousDay, month);
}
- private DayRecord createDay(final JsonDay jsonDay)
+ private DayRecord createDay(final DayData DayData)
{
- return new DayRecord(null, jsonDay, null, null, projectServiceMock);
+ return new DayRecord(null, DayData, null, null, projectServiceMock, modelFactory);
}
- private DayRecord dayRecord(JsonDay day, DayRecord previousDay, MonthIndex month)
+ private DayRecord dayRecord(DayData day, DayRecord previousDay, MonthIndex month)
{
final ContractTermsService contractTerms = contractTerms();
- return new DayRecord(contractTerms, day, previousDay, month, projectServiceMock);
+ return new DayRecord(contractTerms, day, previousDay, month, projectServiceMock, modelFactory);
}
private ContractTermsService contractTerms()
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/MonthIndexTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/MonthIndexTest.java
index 32d1ac33..4269f6f2 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/MonthIndexTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/model/MonthIndexTest.java
@@ -9,12 +9,14 @@
import java.time.Month;
import java.time.YearMonth;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.DayData;
import org.itsallcode.whiterabbit.api.model.DayType;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.Config;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.itsallcode.whiterabbit.logic.test.TestingConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -27,6 +29,8 @@ class MonthIndexTest
@Mock
private ProjectService projectServiceMock;
+ private final ModelFactory modelFactory = new JsonModelFactory();
+
@Test
void testCalculateTotalOvertimeOvertimeNoDays()
{
@@ -76,7 +80,7 @@ void testCalculateTotalOvertimeMultipleDaysNegativeOvertime()
@Test
void testSetOvertimePreviousMonthUpdatesJsonRecord()
{
- final JsonMonth jsonMonth = jsonMonth(Duration.ofHours(1));
+ final MonthData jsonMonth = jsonMonth(Duration.ofHours(1));
final MonthIndex monthIndex = create(jsonMonth);
monthIndex.setOvertimePreviousMonth(Duration.ofHours(2));
@@ -86,7 +90,7 @@ void testSetOvertimePreviousMonthUpdatesJsonRecord()
@Test
void testSetOvertimePreviousMonthUpdatesTotalOvertime()
{
- final JsonMonth jsonMonth = jsonMonth(Duration.ofHours(1));
+ final MonthData jsonMonth = jsonMonth(Duration.ofHours(1));
final MonthIndex monthIndex = create(jsonMonth);
assertThat(monthIndex.getTotalOvertime()).isEqualTo(Duration.ofHours(1));
@@ -127,7 +131,7 @@ void gettingNewDayReturnsEmptyDay()
void gettingDaysDoesNotAddThemToJson()
{
final LocalDate date = LocalDate.of(2020, 5, 4);
- final JsonMonth jsonMonth = jsonMonth(YearMonth.from(date), null);
+ final MonthData jsonMonth = jsonMonth(YearMonth.from(date), null);
final MonthIndex monthIndex = create(TestingConfig.builder().build(), jsonMonth);
final DayRecord day = monthIndex.getDay(date);
@@ -162,24 +166,24 @@ void newDaysHaveCustomWorkingTimeWhenCurrentHoursPerDayAreConfigured()
assertThat(day.getCustomWorkingTime()).isPresent().contains(Duration.ofHours(5));
}
- private Duration calculateTotalOvertime(JsonDay... days)
+ private Duration calculateTotalOvertime(DayData... days)
{
return calculateTotalOvertime(null, days);
}
- private Duration calculateTotalOvertime(Duration overtimePreviousMonth, JsonDay... days)
+ private Duration calculateTotalOvertime(Duration overtimePreviousMonth, DayData... days)
{
return create(jsonMonth(overtimePreviousMonth, days)).getTotalOvertime();
}
- private JsonMonth jsonMonth(Duration overtimePreviousMonth, JsonDay... days)
+ private MonthData jsonMonth(Duration overtimePreviousMonth, DayData... days)
{
return jsonMonth(YearMonth.of(2019, Month.MAY), overtimePreviousMonth, days);
}
- private JsonMonth jsonMonth(YearMonth yearMonth, Duration overtimePreviousMonth, JsonDay... days)
+ private MonthData jsonMonth(YearMonth yearMonth, Duration overtimePreviousMonth, DayData... days)
{
- final JsonMonth month = new JsonMonth();
+ final MonthData month = modelFactory.createMonthData();
month.setYear(yearMonth.getYear());
month.setMonth(yearMonth.getMonth());
month.setDays(asList(days));
@@ -187,12 +191,12 @@ private JsonMonth jsonMonth(YearMonth yearMonth, Duration overtimePreviousMonth,
return month;
}
- private JsonDay day(Duration overtime, int dayOfMonth)
+ private DayData day(Duration overtime, int dayOfMonth)
{
final LocalTime begin = LocalTime.of(8, 0);
final LocalTime end = begin.plus(Duration.ofHours(8)).plus(Duration.ofMinutes(45).plus(overtime));
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(LocalDate.of(2019, Month.MAY, dayOfMonth));
day.setBegin(begin);
day.setEnd(end);
@@ -200,13 +204,13 @@ private JsonDay day(Duration overtime, int dayOfMonth)
return day;
}
- private MonthIndex create(JsonMonth record)
+ private MonthIndex create(MonthData record)
{
return create(TestingConfig.builder().build(), record);
}
- private MonthIndex create(Config config, JsonMonth record)
+ private MonthIndex create(Config config, MonthData record)
{
- return MonthIndex.create(new ContractTermsService(config), record, projectServiceMock);
+ return MonthIndex.create(new ContractTermsService(config), projectServiceMock, modelFactory, record);
}
}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGeneratorTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGeneratorTest.java
index f669d991..bd2cc007 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGeneratorTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/report/vacation/VacationReportGeneratorTest.java
@@ -254,7 +254,7 @@ private void simulateMonths(MonthIndex... months)
{
final List availableDataYearMonth = Arrays.stream(months).map(MonthIndex::getYearMonth)
.collect(toList());
- when(storageMock.getAvailableDataYearMonth()).thenReturn(availableDataYearMonth);
+ when(storageMock.getAvailableDataMonths()).thenReturn(availableDataYearMonth);
for (final MonthIndex monthIndex : months)
{
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ActivityServiceTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ActivityServiceTest.java
index 6f2ba3d5..954ac473 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ActivityServiceTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ActivityServiceTest.java
@@ -14,13 +14,14 @@
import java.util.NoSuchElementException;
import java.util.Optional;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.autocomplete.AutocompleteService;
import org.itsallcode.whiterabbit.logic.model.MonthIndex;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
import org.itsallcode.whiterabbit.logic.storage.Storage;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -41,6 +42,7 @@ class ActivityServiceTest
private ProjectService projectService;
@Mock
private AutocompleteService autocompleteServiceMock;
+ private JsonModelFactory modelFactory;
private ActivityService service;
@@ -48,6 +50,7 @@ class ActivityServiceTest
void setUp()
{
service = new ActivityService(storageMock, autocompleteServiceMock, appServiceCallbackMock);
+ modelFactory = new JsonModelFactory();
}
@Test
@@ -126,10 +129,10 @@ void removeActivity()
private MonthIndex createMonth()
{
- final JsonMonth monthRecord = new JsonMonth();
+ final MonthData monthRecord = modelFactory.createMonthData();
monthRecord.setDays(new ArrayList<>());
monthRecord.setYear(DATE.getYear());
monthRecord.setMonth(DATE.getMonth());
- return MonthIndex.create(contractTermsServiceMock, monthRecord, projectService);
+ return MonthIndex.create(contractTermsServiceMock, projectService, modelFactory, monthRecord);
}
}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/AppServiceTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/AppServiceTest.java
index 35811c61..6bdd6d9e 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/AppServiceTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/AppServiceTest.java
@@ -1,5 +1,6 @@
package org.itsallcode.whiterabbit.logic.service;
+import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -7,6 +8,7 @@
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -17,16 +19,23 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.Month;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Locale;
+import org.itsallcode.whiterabbit.api.model.DayData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
+import org.itsallcode.whiterabbit.api.model.ProjectReport;
import org.itsallcode.whiterabbit.logic.Config;
import org.itsallcode.whiterabbit.logic.autocomplete.AutocompleteService;
import org.itsallcode.whiterabbit.logic.model.DayRecord;
import org.itsallcode.whiterabbit.logic.model.MonthIndex;
-import org.itsallcode.whiterabbit.logic.model.json.JsonDay;
+import org.itsallcode.whiterabbit.logic.model.MultiMonthIndex;
import org.itsallcode.whiterabbit.logic.report.project.ProjectReportGenerator;
+import org.itsallcode.whiterabbit.logic.report.vacation.VacationReport;
import org.itsallcode.whiterabbit.logic.report.vacation.VacationReportGenerator;
import org.itsallcode.whiterabbit.logic.service.AppPropertiesService.AppProperties;
import org.itsallcode.whiterabbit.logic.service.AppServiceCallback.InterruptionDetectedDecision;
@@ -39,6 +48,7 @@
import org.itsallcode.whiterabbit.logic.service.singleinstance.RunningInstanceCallback;
import org.itsallcode.whiterabbit.logic.service.singleinstance.SingleInstanceService;
import org.itsallcode.whiterabbit.logic.storage.Storage;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.itsallcode.whiterabbit.logic.test.TestingConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -82,20 +92,41 @@ class AppServiceTest
private AutocompleteService autocompleteServiceMock;
@Mock
private PluginManager pluginManagerMock;
+ @Mock
+ private WorkingTimeService workingTimeServiceMock;
+
+ private ContractTermsService contractTerms;
private AppService appService;
+ private JsonModelFactory modelFactory;
+
@BeforeEach
void setUp()
{
final DelegatingAppServiceCallback appServiceCallback = new DelegatingAppServiceCallback();
final WorkingTimeService workingTimeService = new WorkingTimeService(storageMock, clockMock,
appServiceCallback);
- appService = new AppService(workingTimeService, storageMock, formatterServiceMock, clockMock,
+ appService = createAppService(appServiceCallback, workingTimeService);
+ appService.setUpdateListener(updateListenerMock);
+ modelFactory = new JsonModelFactory();
+ contractTerms = new ContractTermsService(configMock);
+ }
+
+ private AppService createAppService(final DelegatingAppServiceCallback appServiceCallback,
+ final WorkingTimeService workingTimeService)
+ {
+ return new AppService(workingTimeService, storageMock, formatterServiceMock, clockMock,
schedulingServiceMock, singleInstanceService, appServiceCallback, activityService, projectServiceMock,
autocompleteServiceMock, appPropertiesServiceMock, vacationReportGeneratorMock,
projectReportGeneratorMock, pluginManagerMock);
- appService.setUpdateListener(updateListenerMock);
+ }
+
+ @Test
+ void create()
+ {
+ when(configMock.getLocale()).thenReturn(Locale.ENGLISH);
+ assertThat(AppService.create(configMock)).isNotNull();
}
@Test
@@ -103,7 +134,7 @@ void testNoUpdateWhenNotWorkingDay()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 9);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
updateNow(now, day);
@@ -117,7 +148,7 @@ void testUpdateListenerNotCalledWhenNoUpdate()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 9);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
updateNow(now, day);
@@ -130,7 +161,7 @@ void testUpdateListenerCalledWhenDayRecordUpdated()
{
final LocalTime now = LocalTime.of(8, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
final DayRecord updatedRecord = updateNow(now, day);
@@ -143,7 +174,7 @@ void testUpdateNewDayOnWorkingDay()
{
final LocalTime now = LocalTime.of(8, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
updateNow(now, day);
@@ -158,7 +189,7 @@ void testUpdateExistingDayAfter1Min()
final LocalTime begin = LocalTime.of(8, 0);
final LocalTime now = LocalTime.of(8, 1);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(begin);
day.setEnd(begin);
@@ -175,7 +206,7 @@ void testUpdateExistingDayAfter2Min()
final LocalTime begin = LocalTime.of(8, 0);
final LocalTime now = LocalTime.of(8, 2);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(begin);
day.setEnd(begin);
@@ -196,7 +227,7 @@ void testNoUpdateIfEndAfterNow()
final LocalTime end = LocalTime.of(16, 0);
final LocalTime now = LocalTime.of(9, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(begin);
day.setEnd(end);
@@ -213,7 +244,7 @@ void testUpdateExistingDayAfter3Min()
final LocalTime begin = LocalTime.of(8, 0);
final LocalTime now = LocalTime.of(8, 3);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(begin);
day.setEnd(begin);
@@ -232,7 +263,7 @@ void testUpdateNowAddsInterruption()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(LocalTime.of(13, 0));
@@ -251,7 +282,7 @@ void testUpdateNowDoesNotAddInterruptionWhenCallbackSaysNo()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(LocalTime.of(13, 0));
@@ -271,7 +302,7 @@ void testUpdateNowDoesNotAddInterruptionWhenCallbackStopWorkForToday()
final LocalTime beforeInterruption = LocalTime.of(13, 0);
final LocalTime now = beforeInterruption.plusHours(1);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(beforeInterruption);
@@ -290,7 +321,7 @@ void testUpdateDoesNotUpdateBeginIfInThePast()
{
final LocalTime now = LocalTime.of(9, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
final LocalTime begin = LocalTime.of(8, 0);
day.setBegin(begin);
@@ -307,7 +338,7 @@ void testUpdateDoesUpdateBeginIfInTheFuture()
{
final LocalTime now = LocalTime.of(9, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
final LocalTime begin = LocalTime.of(10, 0);
day.setBegin(begin);
@@ -324,7 +355,7 @@ void testUpdateNowDoesNotAddInterruptionIfAlreadyRunning()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(LocalTime.of(13, 0));
@@ -344,7 +375,7 @@ void addInterruptionCallback()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(LocalTime.of(13, 0));
@@ -368,7 +399,7 @@ void cancelInterruptionCallback()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(LocalTime.of(13, 0));
@@ -434,7 +465,7 @@ void toggleStopWorkForTodayCurrentlyNotStopped()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(now);
@@ -450,7 +481,7 @@ void toggleStopWorkForTodayAlreadyStopped()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(now);
@@ -468,7 +499,7 @@ void endTimeNotUpdatedWhenWorkStoppedForToday()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(now);
@@ -491,7 +522,7 @@ void endTimeNotUpdatedWhenWorkNotStoppedForToday()
{
final LocalTime now = LocalTime.of(14, 0);
final LocalDate today = LocalDate.of(2019, 3, 8);
- final JsonDay day = new JsonDay();
+ final DayData day = modelFactory.createDayData();
day.setDate(today);
day.setBegin(LocalTime.of(8, 0));
day.setEnd(now);
@@ -519,16 +550,221 @@ void loadAppProperties()
assertThat(appService.getAppProperties()).isSameAs(appPropertiesMock);
}
- private DayRecord updateNow(final LocalTime now, final JsonDay day)
+ @Test
+ void updatePreviousMonthOvertimeField_noMonth()
+ {
+ simulateStorageLoadAll();
+ appService.updatePreviousMonthOvertimeField();
+ verify(storageMock, never()).storeMonth(any());
+ }
+
+ @Test
+ void updatePreviousMonthOvertimeField_singleMonthNoDays()
+ {
+ final MonthIndex month1 = monthIndex(YearMonth.of(2021, 1));
+ simulateStorageLoadAll(month1);
+ appService.updatePreviousMonthOvertimeField();
+
+ verify(storageMock).storeMonth(same(month1));
+ assertThat(month1.getOvertimePreviousMonth()).isZero();
+ }
+
+ @Test
+ void updatePreviousMonthOvertimeField_singleMonthWithDays()
+ {
+ final MonthIndex month1 = monthIndex(YearMonth.of(2021, 1),
+ day(LocalDate.of(2021, 1, 1), Duration.ofMinutes(10)));
+ simulateStorageLoadAll(month1);
+ appService.updatePreviousMonthOvertimeField();
+
+ assertThat(month1.getOvertimePreviousMonth()).isZero();
+ }
+
+ @Test
+ void updatePreviousMonthOvertimeField_twoMonthWithSingleDay()
+ {
+ final MonthIndex month1 = monthIndex(YearMonth.of(2021, 1),
+ day(LocalDate.of(2021, 1, 1), Duration.ofMinutes(10)));
+ final MonthIndex month2 = monthIndex(YearMonth.of(2021, 2),
+ day(LocalDate.of(2021, 2, 1), Duration.ofMinutes(20)));
+ simulateStorageLoadAll(month1, month2);
+ appService.updatePreviousMonthOvertimeField();
+
+ assertThat(month1.getOvertimePreviousMonth()).isZero();
+ assertThat(month2.getOvertimePreviousMonth()).isEqualTo(Duration.ofMinutes(10));
+ }
+
+ @Test
+ void updatePreviousMonthOvertimeField_twoMonthWithMultipleDays()
+ {
+ final MonthIndex month1 = monthIndex(YearMonth.of(2021, 1),
+ day(LocalDate.of(2021, 1, 1), Duration.ofMinutes(10)),
+ day(LocalDate.of(2021, 1, 4), Duration.ofMinutes(20)));
+ final MonthIndex month2 = monthIndex(YearMonth.of(2021, 2),
+ day(LocalDate.of(2021, 2, 1), Duration.ofMinutes(5)));
+ simulateStorageLoadAll(month1, month2);
+ appService.updatePreviousMonthOvertimeField();
+
+ assertThat(month1.getOvertimePreviousMonth()).isZero();
+ assertThat(month2.getOvertimePreviousMonth()).isEqualTo(Duration.ofMinutes(30));
+ }
+
+ private void simulateStorageLoadAll(final MonthIndex... months)
+ {
+ when(storageMock.loadAll()).thenReturn(new MultiMonthIndex(asList(months)));
+ }
+
+ private DayData day(LocalDate date, Duration overtime)
+ {
+ final DayData day = modelFactory.createDayData();
+ day.setDate(date);
+ day.setBegin(LocalTime.of(8, 0));
+ day.setEnd(day.getBegin().plus(Duration.ofHours(8).plus(Duration.ofMinutes(45)).plus(overtime)));
+ return day;
+ }
+
+ private MonthIndex monthIndex(YearMonth month, DayData... days)
+ {
+ final MonthData monthData = modelFactory.createMonthData();
+ monthData.setYear(month.getYear());
+ monthData.setMonth(month.getMonth());
+ monthData.setDays(asList(days));
+ return MonthIndex.create(contractTerms, projectServiceMock, modelFactory, monthData);
+ }
+
+ private DayRecord updateNow(final LocalTime now, final DayData day)
{
when(clockMock.getCurrentDate()).thenReturn(day.getDate());
when(clockMock.getCurrentTime()).thenReturn(now);
when(storageMock.loadOrCreate(YearMonth.from(day.getDate()))).thenReturn(monthIndexMock);
final DayRecord dayRecord = new DayRecord(new ContractTermsService(TestingConfig.builder().build()), day, null,
- monthIndexMock, projectServiceMock);
+ monthIndexMock, projectServiceMock, modelFactory);
when(monthIndexMock.getDay(day.getDate())).thenReturn(dayRecord);
appService.updateNow();
return dayRecord;
}
+
+ @Test
+ void store()
+ {
+ final DayData day = modelFactory.createDayData();
+ day.setDate(LocalDate.of(2021, 1, 5));
+ final DayRecord record = new DayRecord(contractTerms, day, null, monthIndexMock,
+ projectServiceMock, modelFactory);
+ final MonthData monthData = modelFactory.createMonthData();
+ monthData.setYear(2021);
+ monthData.setMonth(Month.JANUARY);
+ monthData.setDays(new ArrayList<>());
+ final MonthIndex monthIndex = MonthIndex.create(contractTerms, projectServiceMock, modelFactory,
+ monthData);
+ when(storageMock.loadOrCreate(YearMonth.of(2021, 1))).thenReturn(monthIndex);
+
+ appService.store(record);
+
+ assertThat(monthIndex.getDay(record.getDate())).isSameAs(record);
+ verify(storageMock).storeMonth(same(monthIndex));
+ verify(updateListenerMock).recordUpdated(same(record));
+ }
+
+ @Test
+ void getAvailableDataYearMonth()
+ {
+ final ArrayList list = new ArrayList<>();
+ when(storageMock.getAvailableDataMonths()).thenReturn(list);
+ assertThat(appService.getAvailableDataYearMonth()).isSameAs(list);
+ }
+
+ @Test
+ void getOrCreateMonth()
+ {
+ final YearMonth month = YearMonth.of(2021, 1);
+ when(storageMock.loadOrCreate(month)).thenReturn(monthIndexMock);
+ assertThat(appService.getOrCreateMonth(month)).isSameAs(monthIndexMock);
+ }
+
+ @Test
+ void addInterruption()
+ {
+ final DelegatingAppServiceCallback appServiceCallback = new DelegatingAppServiceCallback();
+ appService = createAppService(appServiceCallback, workingTimeServiceMock);
+ final Duration interruption = Duration.ofMinutes(10);
+ final LocalDate date = LocalDate.of(2021, 1, 1);
+
+ appService.addInterruption(date, interruption);
+
+ verify(workingTimeServiceMock).addInterruption(date, interruption);
+ }
+
+ @Test
+ void generateProjectReport()
+ {
+ final YearMonth month = YearMonth.of(2021, 1);
+ final ProjectReport projectReport = mock(ProjectReport.class);
+ when(projectReportGeneratorMock.generateReport(month)).thenReturn(projectReport);
+
+ assertThat(appService.generateProjectReport(month)).isSameAs(projectReport);
+ verify(projectReportGeneratorMock).generateReport(month);
+ }
+
+ @Test
+ void getVacationReport()
+ {
+ final VacationReport vacationReport = mock(VacationReport.class);
+ when(vacationReportGeneratorMock.generateReport()).thenReturn(vacationReport);
+ assertThat(appService.getVacationReport()).isSameAs(vacationReport);
+ }
+
+ @Test
+ void activities()
+ {
+ assertThat(appService.activities()).isSameAs(activityService);
+ }
+
+ @Test
+ void projects()
+ {
+ assertThat(appService.projects()).isSameAs(projectServiceMock);
+ }
+
+ @Test
+ void autocomplete()
+ {
+ assertThat(appService.autocomplete()).isSameAs(autocompleteServiceMock);
+ }
+
+ @Test
+ void formatter()
+ {
+ assertThat(appService.formatter()).isSameAs(formatterServiceMock);
+ }
+
+ @Test
+ void pluginManager()
+ {
+ assertThat(appService.pluginManager()).isSameAs(pluginManagerMock);
+ }
+
+ @Test
+ void registerSingleInstance_otherInstanceAlreadyRunning()
+ {
+ final RunningInstanceCallback callback = mock(RunningInstanceCallback.class);
+ simulateOtherInstance(callback, true);
+ assertThat(appService.registerSingleInstance(callback)).isPresent();
+ }
+
+ private void simulateOtherInstance(final RunningInstanceCallback callback, boolean otherInstanceRunning)
+ {
+ final RegistrationResult registrationResultMock = mock(RegistrationResult.class);
+ when(registrationResultMock.isOtherInstanceRunning()).thenReturn(otherInstanceRunning);
+ when(singleInstanceService.tryToRegisterInstance(same(callback))).thenReturn(registrationResultMock);
+ }
+
+ @Test
+ void registerSingleInstance_noInstanceAlreadyRunning()
+ {
+ final RunningInstanceCallback callback = mock(RunningInstanceCallback.class);
+ simulateOtherInstance(callback, false);
+ assertThat(appService.registerSingleInstance(callback)).isEmpty();
+ }
}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ClockServiceTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ClockServiceTest.java
new file mode 100644
index 00000000..31ab7e96
--- /dev/null
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/ClockServiceTest.java
@@ -0,0 +1,57 @@
+package org.itsallcode.whiterabbit.logic.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.YearMonth;
+import java.time.ZoneId;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ClockServiceTest
+{
+ private static final Instant NOW = Instant.parse("2007-12-03T10:15:30.00Z");
+ private ClockService clockService;
+
+ @BeforeEach
+ void setUp()
+ {
+ clockService = new ClockService(
+ Clock.fixed(NOW, ZoneId.of("Europe/Berlin")));
+ }
+
+ @Test
+ void testGetCurrentDate()
+ {
+ assertThat(clockService.getCurrentDate()).isEqualTo(LocalDate.of(2007, 12, 3));
+ }
+
+ @Test
+ void testGetCurrentYearMonth()
+ {
+ assertThat(clockService.getCurrentYearMonth()).isEqualTo(YearMonth.of(2007, 12));
+ }
+
+ @Test
+ void testGetCurrentTime()
+ {
+ assertThat(clockService.getCurrentTime()).isEqualTo(LocalTime.of(11, 15));
+ }
+
+ @Test
+ void testGetDurationUntilNextFullMinute()
+ {
+ assertThat(clockService.getDurationUntilNextFullMinute()).isEqualTo(Duration.ofSeconds(30));
+ }
+
+ @Test
+ void testInstant()
+ {
+ assertThat(clockService.instant()).isSameAs(NOW);
+ }
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/FormatterServiceTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/FormatterServiceTest.java
new file mode 100644
index 00000000..cdf590bf
--- /dev/null
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/FormatterServiceTest.java
@@ -0,0 +1,35 @@
+package org.itsallcode.whiterabbit.logic.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Locale;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FormatterServiceTest
+{
+ private FormatterService formatter;
+
+ @BeforeEach
+ void setUp()
+ {
+ formatter = new FormatterService(Locale.GERMAN, ZoneId.of("Europe/Berlin"));
+ }
+
+ @Test
+ void testFormatDuration()
+ {
+ assertThat(formatter.format(Duration.ofHours(4).plusMinutes(42).plusSeconds(31))).isEqualTo("04:42");
+ }
+
+ @Test
+ void testFormatDateAndTime()
+ {
+ assertThat(formatter.formatDateAndTime(Instant.parse("2007-12-03T10:15:30.00Z")))
+ .isEqualTo("03.12.07, 11:15:30");
+ }
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManagerTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManagerTest.java
new file mode 100644
index 00000000..19a845d4
--- /dev/null
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginManagerTest.java
@@ -0,0 +1,151 @@
+package org.itsallcode.whiterabbit.logic.service.plugin;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
+import org.itsallcode.whiterabbit.logic.Config;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PluginManagerTest
+{
+ @Mock
+ private PluginRegistry pluginRegistryMock;
+
+ @Mock
+ private PluginWrapper plugin1;
+ @Mock
+ private PluginWrapper plugin2;
+
+ private PluginManager pluginManager;
+
+ @BeforeEach
+ void setUp()
+ {
+ pluginManager = new PluginManager(pluginRegistryMock);
+ lenient().when(plugin1.getId()).thenReturn("plugin1");
+ lenient().when(plugin2.getId()).thenReturn("plugin2");
+ }
+
+ @Test
+ void create()
+ {
+ final Config config = mock(Config.class);
+ assertThat(PluginManager.create(config)).isNotNull();
+ }
+
+ @Test
+ void getProjectReportExporterPlugins_noPluginAvailable()
+ {
+ simulatePlugins();
+ assertThat(pluginManager.getProjectReportExporterPlugins()).isEmpty();
+ }
+
+ @Test
+ void getProjectReportExporterPlugins_unsupportedPluginAvailable()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(ProjectReportExporter.class)).thenReturn(false);
+ assertThat(pluginManager.getProjectReportExporterPlugins()).isEmpty();
+ }
+
+ @Test
+ void getProjectReportExporterPlugins_supportedPluginAvailable()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(ProjectReportExporter.class)).thenReturn(true);
+ assertThat(pluginManager.getProjectReportExporterPlugins()).containsExactly("plugin1");
+ }
+
+ private void simulatePlugins(PluginWrapper... plugins)
+ {
+ lenient().when(pluginRegistryMock.getAllPlugins()).thenReturn(asList(plugins));
+ for (final PluginWrapper plugin : plugins)
+ {
+ lenient().when(pluginRegistryMock.getPlugin(plugin.getId())).thenReturn(plugin);
+ }
+ }
+
+ @Test
+ void getProjectReportExporter_noPluginAvailable_throwsException()
+ {
+ simulatePlugins();
+ assertThatThrownBy(() -> pluginManager.getProjectReportExporter("unknown"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Plugin 'unknown' not found");
+ }
+
+ @Test
+ void getProjectReportExporter_availableButNotSupported()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(ProjectReportExporter.class)).thenReturn(false);
+ assertThatThrownBy(() -> pluginManager.getProjectReportExporter("plugin1"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Plugin 'plugin1' does not support feature " + ProjectReportExporter.class.getName());
+ }
+
+ @Test
+ void getProjectReportExporter_availableAndSupported()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(ProjectReportExporter.class)).thenReturn(true);
+ final ProjectReportExporter featureMock = mock(ProjectReportExporter.class);
+ when(plugin1.getFeature(ProjectReportExporter.class)).thenReturn(featureMock);
+
+ assertThat(pluginManager.getProjectReportExporter("plugin1")).isNotNull().isSameAs(featureMock);
+ }
+
+ @Test
+ void getMonthDataStorage_singleInstanceAvailable()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(MonthDataStorage.class)).thenReturn(true);
+ final MonthDataStorage featureMock = mock(MonthDataStorage.class);
+ when(plugin1.getFeature(MonthDataStorage.class)).thenReturn(featureMock);
+
+ assertThat(pluginManager.getMonthDataStorage()).isPresent().containsSame(featureMock);
+ }
+
+ @Test
+ void getMonthDataStorage_noInstanceAvailable()
+ {
+ simulatePlugins(plugin1);
+ when(plugin1.supports(MonthDataStorage.class)).thenReturn(false);
+
+ assertThat(pluginManager.getMonthDataStorage()).isEmpty();
+ }
+
+ @Test
+ void getMonthDataStorage_multipleInstancesAvailable()
+ {
+ simulatePlugins(plugin1, plugin2);
+ when(plugin1.supports(MonthDataStorage.class)).thenReturn(true);
+ when(plugin2.supports(MonthDataStorage.class)).thenReturn(true);
+
+ assertThatThrownBy(() -> pluginManager.getMonthDataStorage())
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage(
+ "Found multiple plugins supporting " + MonthDataStorage.class.getName()
+ + ": [plugin1, plugin2]. Please add only one storage plugin to the classpath.");
+ }
+
+ @Test
+ void close()
+ {
+ pluginManager.close();
+ verify(pluginRegistryMock).close();
+ }
+
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistryTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistryTest.java
new file mode 100644
index 00000000..b6452bef
--- /dev/null
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/PluginRegistryTest.java
@@ -0,0 +1,136 @@
+package org.itsallcode.whiterabbit.logic.service.plugin;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+
+import org.itsallcode.whiterabbit.logic.Config;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PluginRegistryTest
+{
+ @Mock
+ private Config configMock;
+ private PluginRegistry pluginRegistry;
+
+ @BeforeEach
+ void setUp()
+ {
+ pluginRegistry = new PluginRegistry(configMock);
+ }
+
+ @Test
+ void loadFromEmptyPluginDir(@TempDir Path tempDir)
+ {
+ when(configMock.getPluginDir()).thenReturn(tempDir);
+ pluginRegistry.load();
+ assertThat(pluginRegistry.getAllPlugins()).hasSize(1);
+ }
+
+ @Test
+ void loadFromNonExistingPluginDir()
+ {
+ when(configMock.getPluginDir()).thenReturn(Paths.get("no-such-directory"));
+ pluginRegistry.load();
+ assertThat(pluginRegistry.getAllPlugins()).hasSize(1);
+ }
+
+ @Test
+ void loadInvalidJar(@TempDir Path tempDir) throws IOException
+ {
+ when(configMock.getPluginDir()).thenReturn(tempDir);
+ Files.writeString(tempDir.resolve("invalid-plugin.jar"), "");
+ pluginRegistry.load();
+ assertThat(pluginRegistry.getAllPlugins()).hasSize(1);
+ }
+
+ @Test
+ void getAllPlugins_returnsEmptyList_WhenNotLoadedYet()
+ {
+ assertThat(pluginRegistry.getAllPlugins()).isEmpty();
+ }
+
+ @Test
+ void getAllPlugins_returnsPlugins_WhenLoaded()
+ {
+ pluginRegistry.load();
+ final Collection plugins = pluginRegistry.getAllPlugins();
+ assertThat(plugins).hasSize(1);
+ final PluginWrapper plugin = plugins.iterator().next();
+ verifyTestingPlugin(plugin);
+ }
+
+ private void verifyTestingPlugin(final PluginWrapper plugin)
+ {
+ final TestingPlugin pluginInstance = plugin.getFeature(TestingPlugin.class);
+ assertThat(pluginInstance).isInstanceOf(TestingPlugin.class);
+ assertThat(pluginInstance.getId()).isEqualTo(TestingPlugin.PLUGIN_ID);
+ assertThat(pluginInstance.isClosed()).isFalse();
+ }
+
+ @Test
+ void getPlugin_failsWhenNotLoadedAndPluginNotAvailable()
+ {
+ assertThatThrownBy(() -> pluginRegistry.getPlugin("unknown"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Plugin 'unknown' not found. Available plugins: []");
+ }
+
+ @Test
+ void getPlugin_failsWhenLoadedAndPluginNotAvailable()
+ {
+ pluginRegistry.load();
+ assertThatThrownBy(() -> pluginRegistry.getPlugin("unknown"))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Plugin 'unknown' not found. Available plugins: [" + TestingPlugin.PLUGIN_ID + "]");
+ }
+
+ @Test
+ void getPlugin_returnsPluginWhenAvailable()
+ {
+ pluginRegistry.load();
+ verifyTestingPlugin(pluginRegistry.getPlugin(TestingPlugin.PLUGIN_ID));
+ }
+
+ @Test
+ void pluginConfig_getMandatoryValue_ReturnsValue()
+ {
+ pluginRegistry.load();
+ final TestingPlugin pluginInstance = pluginRegistry.getPlugin(TestingPlugin.PLUGIN_ID)
+ .getFeature(TestingPlugin.class);
+
+ when(configMock.getMandatoryValue(TestingPlugin.PLUGIN_ID + ".property")).thenReturn("propertyValue");
+ assertThat(pluginInstance.getConfig().getMandatoryValue("property")).isEqualTo("propertyValue");
+ }
+
+ @Test
+ void close_whenNoPluginLoaded()
+ {
+ assertDoesNotThrow(() -> pluginRegistry.close());
+ }
+
+ @Test
+ void close_closesPlugins()
+ {
+ pluginRegistry.load();
+ final TestingPlugin plugin = pluginRegistry.getPlugin(TestingPlugin.PLUGIN_ID).getFeature(TestingPlugin.class);
+ assertThat(plugin.isClosed()).isFalse();
+
+ pluginRegistry.close();
+
+ assertThat(plugin.isClosed()).isTrue();
+ }
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/TestingPlugin.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/TestingPlugin.java
new file mode 100644
index 00000000..f73e709d
--- /dev/null
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/service/plugin/TestingPlugin.java
@@ -0,0 +1,52 @@
+package org.itsallcode.whiterabbit.logic.service.plugin;
+
+import org.itsallcode.whiterabbit.api.Plugin;
+import org.itsallcode.whiterabbit.api.PluginConfiguration;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
+
+public class TestingPlugin implements Plugin, PluginFeature
+{
+ static final String PLUGIN_ID = "testingPlugin";
+ private PluginConfiguration config;
+ private boolean closed = false;
+
+ @Override
+ public void init(PluginConfiguration config)
+ {
+ this.config = config;
+ }
+
+ @Override
+ public String getId()
+ {
+ return PLUGIN_ID;
+ }
+
+ @Override
+ public void close()
+ {
+ this.closed = true;
+ }
+
+ @Override
+ public boolean supports(Class extends PluginFeature> featureType)
+ {
+ return TestingPlugin.class.isAssignableFrom(featureType);
+ }
+
+ @Override
+ public T getFeature(Class featureType)
+ {
+ return featureType.cast(this);
+ }
+
+ public PluginConfiguration getConfig()
+ {
+ return config;
+ }
+
+ public boolean isClosed()
+ {
+ return closed;
+ }
+}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImplTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImplTest.java
index 81934975..1dbb03be 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImplTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/CachingStorageImplTest.java
@@ -99,7 +99,7 @@ void loadAll_doesNotUpdateCache_whenNoEntriesFound()
@Test
void getRequiredYearMonths_returnsEmptyList_whenNoDataAvailable()
{
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(emptyList());
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(emptyList());
assertThat(storage.getRequiredYearMonths(LocalDate.of(2020, Month.JANUARY, 1))).isEmpty();
}
@@ -111,7 +111,7 @@ void getRequiredYearMonths_returnsRequiredMonths()
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
final YearMonth march = YearMonth.of(2020, Month.MARCH);
final YearMonth april = YearMonth.of(2020, Month.APRIL);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february, march, april));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february, march, april));
assertThat(storage.getRequiredYearMonths(LocalDate.of(2020, Month.JANUARY, 1)))
.containsExactly(january, february, march, april);
@@ -124,7 +124,7 @@ void getRequiredYearMonths_includesMonthForLastDayOfTheMonth()
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
final YearMonth march = YearMonth.of(2020, Month.MARCH);
final YearMonth april = YearMonth.of(2020, Month.APRIL);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february, march, april));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february, march, april));
assertThat(storage.getRequiredYearMonths(LocalDate.of(2020, Month.JANUARY, 31)))
.containsExactly(january, february, march, april);
@@ -137,7 +137,7 @@ void getRequiredYearMonths_omitsOlderMonths()
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
final YearMonth march = YearMonth.of(2020, Month.MARCH);
final YearMonth april = YearMonth.of(2020, Month.APRIL);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february, march, april));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february, march, april));
assertThat(storage.getRequiredYearMonths(LocalDate.of(2020, Month.FEBRUARY, 1)))
.containsExactly(february, march, april);
@@ -148,7 +148,7 @@ void ensureLatestDaysCached_dataAvailable()
{
final YearMonth january = YearMonth.of(2020, Month.JANUARY);
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february));
when(cacheMock.contains(january)).thenReturn(false);
when(cacheMock.contains(february)).thenReturn(false);
@@ -172,7 +172,7 @@ void ensureLatestDaysCached_dataAvailable_alreadyCached()
{
final YearMonth january = YearMonth.of(2020, Month.JANUARY);
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february));
when(cacheMock.contains(january)).thenReturn(true);
when(cacheMock.contains(february)).thenReturn(true);
@@ -188,7 +188,7 @@ void ensureLatestDaysCached_cacheNotUpdatedWhenMonthNotFound()
{
final YearMonth january = YearMonth.of(2020, Month.JANUARY);
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february));
when(delegateStorageMock.loadMonth(january)).thenReturn(Optional.empty());
when(delegateStorageMock.loadMonth(february)).thenReturn(Optional.empty());
@@ -204,7 +204,7 @@ void getLatestDays_delegatesToCache()
final YearMonth january = YearMonth.of(2020, Month.JANUARY);
final YearMonth february = YearMonth.of(2020, Month.FEBRUARY);
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(List.of(january, february));
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(List.of(january, february));
when(cacheMock.contains(january)).thenReturn(false);
when(cacheMock.contains(february)).thenReturn(false);
@@ -237,7 +237,7 @@ private void verifyListenerUpdated()
void getAvailableDataYearMonth_delegates()
{
final List list = new ArrayList<>();
- when(delegateStorageMock.getAvailableDataYearMonth()).thenReturn(list);
- assertThat(storage.getAvailableDataYearMonth()).isSameAs(list);
+ when(delegateStorageMock.getAvailableDataMonths()).thenReturn(list);
+ assertThat(storage.getAvailableDataMonths()).isSameAs(list);
}
}
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorageTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorageTest.java
index f010aa1a..cfb920a0 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorageTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/MonthIndexStorageTest.java
@@ -4,6 +4,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -14,11 +15,13 @@
import java.util.List;
import java.util.Optional;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.itsallcode.whiterabbit.logic.model.MonthIndex;
import org.itsallcode.whiterabbit.logic.model.MultiMonthIndex;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
import org.itsallcode.whiterabbit.logic.service.contract.ContractTermsService;
import org.itsallcode.whiterabbit.logic.service.project.ProjectService;
+import org.itsallcode.whiterabbit.logic.storage.data.JsonModelFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -36,7 +39,7 @@ class MonthIndexStorageTest
@Mock
ProjectService projectServiceMock;
@Mock
- JsonFileStorage fileStorageMock;
+ MonthDataStorage fileStorageMock;
@Mock
MonthIndex monthIndexMock;
@@ -46,12 +49,13 @@ class MonthIndexStorageTest
void setUp()
{
storage = new MonthIndexStorage(contractTermsMock, projectServiceMock, fileStorageMock);
+ lenient().when(fileStorageMock.getModelFactory()).thenReturn(new JsonModelFactory());
}
@Test
void loadPreviousMonthOvertime_returnsZero_whenNoPreviousMonth()
{
- when(fileStorageMock.loadMonthRecord(YearMonth.of(2020, Month.OCTOBER))).thenReturn(Optional.empty());
+ when(fileStorageMock.load(YearMonth.of(2020, Month.OCTOBER))).thenReturn(Optional.empty());
assertThat(storage.loadPreviousMonthOvertime(YEAR_MONTH)).isZero();
}
@@ -59,7 +63,7 @@ void loadPreviousMonthOvertime_returnsZero_whenNoPreviousMonth()
@Test
void loadPreviousMonthOvertime_returnsPreviousMonthOvertime()
{
- when(fileStorageMock.loadMonthRecord(PREVIOUS_MONTH))
+ when(fileStorageMock.load(PREVIOUS_MONTH))
.thenReturn(Optional.of(jsonMonth(PREVIOUS_MONTH, Duration.ofMinutes(5))));
assertThat(storage.loadPreviousMonthOvertime(YEAR_MONTH)).hasMinutes(5);
@@ -68,7 +72,7 @@ void loadPreviousMonthOvertime_returnsPreviousMonthOvertime()
@Test
void loadPreviousMonthOvertime_truncatesToMinute()
{
- when(fileStorageMock.loadMonthRecord(PREVIOUS_MONTH))
+ when(fileStorageMock.load(PREVIOUS_MONTH))
.thenReturn(Optional.of(jsonMonth(PREVIOUS_MONTH, Duration.ofMinutes(5).plusSeconds(50))));
assertThat(storage.loadPreviousMonthOvertime(YEAR_MONTH)).hasMinutes(5);
@@ -77,7 +81,7 @@ void loadPreviousMonthOvertime_truncatesToMinute()
@Test
void loadOrCreate_createsNewMonthIfNotExists()
{
- when(fileStorageMock.loadMonthRecord(YEAR_MONTH)).thenReturn(Optional.empty());
+ when(fileStorageMock.load(YEAR_MONTH)).thenReturn(Optional.empty());
final MonthIndex newMonth = storage.loadOrCreate(YEAR_MONTH);
@@ -90,8 +94,8 @@ void loadOrCreate_createsNewMonthIfNotExists()
@Test
void loadOrCreate_loadsExistingMonth()
{
- final JsonMonth month = jsonMonth(YEAR_MONTH, Duration.ofMinutes(4));
- when(fileStorageMock.loadMonthRecord(YEAR_MONTH)).thenReturn(Optional.of(month));
+ final MonthData month = jsonMonth(YEAR_MONTH, Duration.ofMinutes(4));
+ when(fileStorageMock.load(YEAR_MONTH)).thenReturn(Optional.of(month));
final MonthIndex newMonth = storage.loadOrCreate(YEAR_MONTH);
@@ -105,13 +109,13 @@ void loadOrCreate_loadsExistingMonth()
@Test
void storeMonth()
{
- final JsonMonth month = jsonMonth(YEAR_MONTH, Duration.ofMinutes(4));
+ final MonthData month = jsonMonth(YEAR_MONTH, Duration.ofMinutes(4));
when(monthIndexMock.getMonthRecord()).thenReturn(month);
when(monthIndexMock.getYearMonth()).thenReturn(YEAR_MONTH);
storage.storeMonth(monthIndexMock);
- verify(fileStorageMock).writeToFile(eq(YEAR_MONTH), same(month));
+ verify(fileStorageMock).store(eq(YEAR_MONTH), same(month));
}
@Test
@@ -140,14 +144,14 @@ void loadAll_nonEmpty()
void getAvailableDataYearMonth()
{
final List availableYearMonths = List.of(YEAR_MONTH);
- when(fileStorageMock.getAvailableDataYearMonth()).thenReturn(availableYearMonths);
+ when(fileStorageMock.getAvailableDataMonths()).thenReturn(availableYearMonths);
- assertThat(storage.getAvailableDataYearMonth()).isSameAs(availableYearMonths);
+ assertThat(storage.getAvailableDataMonths()).isSameAs(availableYearMonths);
}
- private JsonMonth jsonMonth(YearMonth yearMonth, Duration overtimePreviousMonth)
+ private MonthData jsonMonth(YearMonth yearMonth, Duration overtimePreviousMonth)
{
- final JsonMonth month = new JsonMonth();
+ final MonthData month = new JsonModelFactory().createMonthData();
month.setYear(yearMonth.getYear());
month.setMonth(yearMonth.getMonth());
month.setOvertimePreviousMonth(overtimePreviousMonth);
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapperTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapperTest.java
similarity index 98%
rename from logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapperTest.java
rename to logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapperTest.java
index cbf7af96..8d303f72 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/DateToFileMapperTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/DateToFileMapperTest.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.storage;
+package org.itsallcode.whiterabbit.logic.storage.data;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
diff --git a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorageTest.java b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorageTest.java
similarity index 63%
rename from logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorageTest.java
rename to logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorageTest.java
index 863c9f98..bcc36a84 100644
--- a/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/JsonFileStorageTest.java
+++ b/logic/src/test/java/org/itsallcode/whiterabbit/logic/storage/data/JsonFileStorageTest.java
@@ -1,4 +1,4 @@
-package org.itsallcode.whiterabbit.logic.storage;
+package org.itsallcode.whiterabbit.logic.storage.data;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -16,10 +16,10 @@
import java.util.stream.Stream;
import javax.json.bind.Jsonb;
-import javax.json.bind.JsonbBuilder;
-import javax.json.bind.JsonbConfig;
-import org.itsallcode.whiterabbit.logic.model.json.JsonMonth;
+import org.itsallcode.whiterabbit.api.features.MonthDataStorage.ModelFactory;
+import org.itsallcode.whiterabbit.api.model.ActivityData;
+import org.itsallcode.whiterabbit.api.model.MonthData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -35,6 +35,8 @@ class JsonFileStorageTest
Path tempDir;
@Mock
DateToFileMapper dateToFileMapperMock;
+ @Mock
+ private ModelFactory modelFactoryMock;
JsonFileStorage jsonFileStorage;
Jsonb jsonb;
@@ -42,8 +44,8 @@ class JsonFileStorageTest
@BeforeEach
void setUp()
{
- jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(false));
- jsonFileStorage = new JsonFileStorage(jsonb, dateToFileMapperMock);
+ jsonb = new JsonbFactory().createNonFormatting();
+ jsonFileStorage = new JsonFileStorage(jsonb, dateToFileMapperMock, modelFactoryMock);
}
@Test
@@ -53,14 +55,14 @@ void sortsAvailableYearMonths()
final YearMonth january2020 = YearMonth.of(2020, Month.JANUARY);
final YearMonth december2019 = YearMonth.of(2019, Month.DECEMBER);
when(dateToFileMapperMock.getAllYearMonths()).thenReturn(Stream.of(april2020, january2020, december2019));
- assertThat(jsonFileStorage.getAvailableDataYearMonth()).containsExactly(december2019, january2020, april2020);
+ assertThat(jsonFileStorage.getAvailableDataMonths()).containsExactly(december2019, january2020, april2020);
}
@Test
void returnsEmptyAvailableMonths()
{
when(dateToFileMapperMock.getAllYearMonths()).thenReturn(Stream.empty());
- assertThat(jsonFileStorage.getAvailableDataYearMonth()).isEmpty();
+ assertThat(jsonFileStorage.getAvailableDataMonths()).isEmpty();
}
@Test
@@ -68,19 +70,19 @@ void loadMonthReturnsEmptyOptional_WhenLegacyFileDoesNotExist()
{
when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(tempDir.resolve("does-not-exist"));
when(dateToFileMapperMock.getLegacyPathForDate(YEAR_MONTH)).thenReturn(tempDir.resolve("does-not-exist"));
- assertThat(jsonFileStorage.loadMonthRecord(YEAR_MONTH)).isEmpty();
+ assertThat(jsonFileStorage.load(YEAR_MONTH)).isEmpty();
}
@Test
void loadMonthReturnsMonth_WhenLegacyFileExists() throws IOException
{
- final JsonMonth month = new JsonMonth();
+ final MonthData month = new JsonMonth();
month.setYear(2020);
final Path file = writeTempFile(month);
when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(tempDir.resolve("does-not-exist"));
when(dateToFileMapperMock.getLegacyPathForDate(YEAR_MONTH)).thenReturn(file);
- final Optional loadedMonth = jsonFileStorage.loadMonthRecord(YEAR_MONTH);
+ final Optional loadedMonth = jsonFileStorage.load(YEAR_MONTH);
assertThat(loadedMonth).isNotEmpty();
assertThat(loadedMonth.get().getYear()).isEqualTo(2020);
}
@@ -88,25 +90,60 @@ void loadMonthReturnsMonth_WhenLegacyFileExists() throws IOException
@Test
void loadMonthReturnsMonth_WhenFileExists() throws IOException
{
- final JsonMonth month = new JsonMonth();
+ final MonthData month = new JsonMonth();
month.setYear(2020);
final Path file = writeTempFile(month);
when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(file);
- final Optional loadedMonth = jsonFileStorage.loadMonthRecord(YEAR_MONTH);
+ final Optional loadedMonth = jsonFileStorage.load(YEAR_MONTH);
assertThat(loadedMonth).isNotEmpty();
assertThat(loadedMonth.get().getYear()).isEqualTo(2020);
}
+ @Test
+ void loadMonthReturnsMonth_WithDays() throws IOException
+ {
+ final MonthData month = new JsonMonth();
+ month.setYear(2020);
+ final JsonDay day = new JsonDay();
+ day.setComment("comment");
+ month.setDays(List.of(day));
+ final Path file = writeTempFile(month);
+
+ when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(file);
+ final Optional loadedMonth = jsonFileStorage.load(YEAR_MONTH);
+ assertThat(loadedMonth.get().getDays()).hasSize(1);
+ assertThat(loadedMonth.get().getDays().get(0).getComment()).isEqualTo("comment");
+ }
+
+ @Test
+ void loadMonthReturnsMonth_WithActivities() throws IOException
+ {
+ final MonthData month = new JsonMonth();
+ month.setYear(2020);
+ final JsonDay day = new JsonDay();
+ final JsonActivity activity = new JsonActivity();
+ activity.setProjectId("project");
+ day.setActivities(List.of(activity));
+ month.setDays(List.of(day));
+ final Path file = writeTempFile(month);
+
+ when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(file);
+ final Optional loadedMonth = jsonFileStorage.load(YEAR_MONTH);
+ final List activities = loadedMonth.get().getDays().get(0).getActivities();
+ assertThat(activities).hasSize(1);
+ assertThat(activities.get(0).getProjectId()).isEqualTo("project");
+ }
+
@Test
void writeToFile_writesFile() throws IOException
{
final Path file = createTempFile();
when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(file);
- final JsonMonth month = new JsonMonth();
+ final MonthData month = new JsonMonth();
month.setYear(2020);
- jsonFileStorage.writeToFile(YEAR_MONTH, month);
+ jsonFileStorage.store(YEAR_MONTH, month);
assertThat(file).exists().hasContent("{\"year\":2020}");
}
@@ -117,9 +154,9 @@ void writeToFile_createsDirectory() throws IOException
final Path file = tempDir.resolve("sub-dir1").resolve("sub-dir2").resolve("file.json");
when(dateToFileMapperMock.getPathForDate(YEAR_MONTH)).thenReturn(file);
- final JsonMonth month = new JsonMonth();
+ final MonthData month = new JsonMonth();
month.setYear(2020);
- jsonFileStorage.writeToFile(YEAR_MONTH, month);
+ jsonFileStorage.store(YEAR_MONTH, month);
assertThat(file).exists().hasContent("{\"year\":2020}");
}
@@ -148,12 +185,12 @@ void loadAll_returnsFiles() throws IOException
final Path file2 = tempDir.resolve("file2.json");
when(dateToFileMapperMock.getAllFiles()).thenReturn(Stream.of(file1, file2));
- final JsonMonth month1 = month(2019, Month.DECEMBER);
- final JsonMonth month2 = month(2020, Month.JANUARY);
+ final MonthData month1 = month(2019, Month.DECEMBER);
+ final MonthData month2 = month(2020, Month.JANUARY);
writeMonth(month1, file1);
writeMonth(month2, file2);
- final List months = jsonFileStorage.loadAll();
+ final List months = jsonFileStorage.loadAll();
assertThat(months).hasSize(2);
assertMonth(months.get(0), month1);
@@ -168,14 +205,14 @@ void loadAll_sortsByYearMonth() throws IOException
final Path file3 = tempDir.resolve("file3.json");
when(dateToFileMapperMock.getAllFiles()).thenReturn(Stream.of(file3, file2, file1));
- final JsonMonth month1 = month(2019, Month.DECEMBER);
- final JsonMonth month2 = month(2020, Month.JANUARY);
- final JsonMonth month3 = month(2020, Month.APRIL);
+ final MonthData month1 = month(2019, Month.DECEMBER);
+ final MonthData month2 = month(2020, Month.JANUARY);
+ final MonthData month3 = month(2020, Month.APRIL);
writeMonth(month1, file1);
writeMonth(month2, file2);
writeMonth(month3, file3);
- final List months = jsonFileStorage.loadAll();
+ final List months = jsonFileStorage.loadAll();
assertThat(months).hasSize(3);
assertMonth(months.get(0), month1);
@@ -183,28 +220,34 @@ void loadAll_sortsByYearMonth() throws IOException
assertMonth(months.get(2), month3);
}
- private void assertMonth(JsonMonth actual, JsonMonth expected)
+ @Test
+ void getModelFactory()
+ {
+ assertThat(jsonFileStorage.getModelFactory()).isSameAs(modelFactoryMock);
+ }
+
+ private void assertMonth(MonthData actual, MonthData expected)
{
assertAll(() -> assertThat(actual.getYear()).isEqualTo(expected.getYear()),
() -> assertThat(actual.getMonth()).isEqualTo(expected.getMonth()));
}
- private JsonMonth month(int year, Month month)
+ private MonthData month(int year, Month month)
{
- final JsonMonth jsonMonth = new JsonMonth();
+ final MonthData jsonMonth = new JsonMonth();
jsonMonth.setYear(year);
jsonMonth.setMonth(month);
return jsonMonth;
}
- private Path writeTempFile(JsonMonth month) throws IOException
+ private Path writeTempFile(MonthData month) throws IOException
{
final Path file = createTempFile();
writeMonth(month, file);
return file;
}
- private void writeMonth(JsonMonth month, final Path file) throws IOException
+ private void writeMonth(MonthData month, final Path file) throws IOException
{
Files.writeString(file, jsonb.toJson(month));
}
diff --git a/logic/src/test/resources/META-INF/services/org.itsallcode.whiterabbit.api.Plugin b/logic/src/test/resources/META-INF/services/org.itsallcode.whiterabbit.api.Plugin
new file mode 100644
index 00000000..f5e3c6e3
--- /dev/null
+++ b/logic/src/test/resources/META-INF/services/org.itsallcode.whiterabbit.api.Plugin
@@ -0,0 +1 @@
+org.itsallcode.whiterabbit.logic.service.plugin.TestingPlugin
diff --git a/plugins/build.gradle b/plugins/build.gradle
index d343eb8e..df89f59d 100644
--- a/plugins/build.gradle
+++ b/plugins/build.gradle
@@ -11,6 +11,12 @@ subprojects {
archiveClassifier.set(null)
archiveVersion.set("")
}
+
+ eclipse {
+ classpath {
+ plusConfigurations += [ configurations.compileOnly ]
+ }
+ }
}
def pluginBuildTasks = project.subprojects.collect {it.tasks['shadowJar']}
diff --git a/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoPlugin.java b/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoPlugin.java
index 467b7ad8..73fcfe96 100644
--- a/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoPlugin.java
+++ b/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoPlugin.java
@@ -2,6 +2,7 @@
import org.itsallcode.whiterabbit.api.Plugin;
import org.itsallcode.whiterabbit.api.PluginConfiguration;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
public class DemoPlugin implements Plugin
{
@@ -24,13 +25,13 @@ public void close()
}
@Override
- public boolean supports(Class> featureType)
+ public boolean supports(Class extends PluginFeature> featureType)
{
return featureType.isAssignableFrom(DemoProjectReportExporter.class);
}
@Override
- public T getFeature(Class featureType)
+ public T getFeature(Class featureType)
{
if (featureType.isAssignableFrom(DemoProjectReportExporter.class))
{
diff --git a/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoProjectReportExporter.java b/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoProjectReportExporter.java
index f0099073..c3d22113 100644
--- a/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoProjectReportExporter.java
+++ b/plugins/demo/src/main/java/org/itsallcode/whiterabbit/plugin/demo/DemoProjectReportExporter.java
@@ -2,8 +2,8 @@
import java.time.Duration;
-import org.itsallcode.whiterabbit.api.ProgressMonitor;
-import org.itsallcode.whiterabbit.api.ProjectReportExporter;
+import org.itsallcode.whiterabbit.api.features.ProgressMonitor;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
import org.itsallcode.whiterabbit.api.model.ProjectReport;
import org.itsallcode.whiterabbit.api.model.ProjectReportActivity;
import org.itsallcode.whiterabbit.api.model.ProjectReportDay;
diff --git a/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExportPlugin.java b/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExportPlugin.java
index a36fa781..563d661a 100644
--- a/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExportPlugin.java
+++ b/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExportPlugin.java
@@ -2,6 +2,7 @@
import org.itsallcode.whiterabbit.api.Plugin;
import org.itsallcode.whiterabbit.api.PluginConfiguration;
+import org.itsallcode.whiterabbit.api.features.PluginFeature;
import org.itsallcode.whiterabbit.plugin.pmsmart.web.WebDriverFactory;
public class PMSmartExportPlugin implements Plugin
@@ -15,13 +16,13 @@ public void init(PluginConfiguration config)
}
@Override
- public boolean supports(Class> featureType)
+ public boolean supports(Class extends PluginFeature> featureType)
{
return featureType.isAssignableFrom(PMSmartExporter.class);
}
@Override
- public T getFeature(Class featureType)
+ public T getFeature(Class featureType)
{
if (featureType.isAssignableFrom(PMSmartExporter.class))
{
diff --git a/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExporter.java b/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExporter.java
index 38a7508b..eb11e7ed 100644
--- a/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExporter.java
+++ b/plugins/pmsmart/src/main/java/org/itsallcode/whiterabbit/plugin/pmsmart/PMSmartExporter.java
@@ -3,8 +3,8 @@
import java.util.Map;
import org.itsallcode.whiterabbit.api.PluginConfiguration;
-import org.itsallcode.whiterabbit.api.ProgressMonitor;
-import org.itsallcode.whiterabbit.api.ProjectReportExporter;
+import org.itsallcode.whiterabbit.api.features.ProgressMonitor;
+import org.itsallcode.whiterabbit.api.features.ProjectReportExporter;
import org.itsallcode.whiterabbit.api.model.ProjectReport;
import org.itsallcode.whiterabbit.api.model.ProjectReportActivity;
import org.itsallcode.whiterabbit.api.model.ProjectReportDay;