From 70a4f4cf24918699af91e1f51541c69e4b2e7716 Mon Sep 17 00:00:00 2001 From: Marcus Fihlon Date: Mon, 14 Oct 2024 13:27:00 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Working=20on=20Header=20and=20Fo?= =?UTF-8?q?oter=20#666=20#667?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../komunumo/configuration/Configuration.java | 28 ++++++ .../komunumo/ui/website/WebsiteFooter.java | 77 ++++++++++++++++ .../komunumo/ui/website/WebsiteHeader.java | 7 +- .../komunumo/ui/website/WebsiteLayout.java | 5 +- .../org/komunumo/ui/website/WebsiteLogo.java | 62 +++++++++++++ .../org/komunumo/ui/website/WebsiteStats.java | 41 +++++++++ .../komunumo/ui/website/home/HomeView.java | 8 +- .../java/org/komunumo/util/FormatterUtil.java | 45 +++++++++ .../db/migration/V1_0_1__configuration.sql | 10 +- .../ui/website/WebsiteLayoutTest.java | 20 ++-- .../komunumo/ui/website/WebsiteLogoTest.java | 91 +++++++++++++++++++ .../komunumo/ui/website/home/HomeViewIT.java | 6 +- .../org/komunumo/util/FormatterUtilTest.java | 59 ++++++++++++ 13 files changed, 439 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/komunumo/ui/website/WebsiteFooter.java create mode 100644 src/main/java/org/komunumo/ui/website/WebsiteLogo.java create mode 100644 src/main/java/org/komunumo/ui/website/WebsiteStats.java create mode 100644 src/main/java/org/komunumo/util/FormatterUtil.java create mode 100644 src/test/java/org/komunumo/ui/website/WebsiteLogoTest.java create mode 100644 src/test/java/org/komunumo/util/FormatterUtilTest.java diff --git a/src/main/java/org/komunumo/configuration/Configuration.java b/src/main/java/org/komunumo/configuration/Configuration.java index c02dec7b..21cb4518 100644 --- a/src/main/java/org/komunumo/configuration/Configuration.java +++ b/src/main/java/org/komunumo/configuration/Configuration.java @@ -37,4 +37,32 @@ public String getWebsiteName() { return data.getOrDefault("website.name", ""); } + public String getWebsiteContactAddress() { + return data.getOrDefault("website.contact.address", ""); + } + + public String getWebsiteContactEmail() { + return data.getOrDefault("website.contact.email", "noreply@localhost"); + } + + public String getWebsiteCopyright() { + return data.getOrDefault("website.copyright", ""); + } + + public String getWebsiteAboutText() { + return data.getOrDefault("website.about.text", ""); + } + + public String getWebsiteLogoTemplate() { + return data.getOrDefault("website.logo.template", ""); + } + + public int getWebsiteMinLogoNumber() { + return Integer.parseInt(data.getOrDefault("website.logo.min", "0")); + } + + public int getWebsiteMaxLogoNumber() { + return Integer.parseInt(data.getOrDefault("website.logo.max", "0")); + } + } diff --git a/src/main/java/org/komunumo/ui/website/WebsiteFooter.java b/src/main/java/org/komunumo/ui/website/WebsiteFooter.java new file mode 100644 index 00000000..9cf276b0 --- /dev/null +++ b/src/main/java/org/komunumo/ui/website/WebsiteFooter.java @@ -0,0 +1,77 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.ui.website; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.html.Anchor; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Footer; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.jetbrains.annotations.NotNull; +import org.komunumo.configuration.Configuration; +import org.komunumo.data.service.DatabaseService; + +public class WebsiteFooter extends Footer { + + private final transient Configuration configuration; + + public WebsiteFooter(@NotNull final DatabaseService databaseService) { + this.configuration = databaseService.configuration(); + setId("website-footer"); + + add( + createAbout(), + createContact() + ); + } + + private Component createAbout() { + final var layout = new HorizontalLayout(); + layout.setId("website-footer-about"); + + final var title = new Div(new H2("About")); + final var about = new Html("
%s
".formatted(configuration.getWebsiteAboutText())); + layout.add(new HorizontalLayout(title, about)); + + return layout; + } + + private Component createContact() { + final var layout = new HorizontalLayout(); + layout.setId("website-footer-contact"); + + final var title = new H2("Contact"); + final var name = new Div(new Text(configuration.getWebsiteName())); + final var address = new Div(new Text(configuration.getWebsiteContactAddress())); + final var email = createEmail(configuration.getWebsiteContactEmail()); + final var copyright = new Div(new Text(configuration.getWebsiteCopyright())); + layout.add(new HorizontalLayout(title, new Div(name, address, email, copyright))); + + return layout; + } + + private Component createEmail(@NotNull final String email) { + final var div = new Div(new Anchor(String.format("mailto:%s", email), email)); + div.addClassName("contact-email"); + return div; + } + +} diff --git a/src/main/java/org/komunumo/ui/website/WebsiteHeader.java b/src/main/java/org/komunumo/ui/website/WebsiteHeader.java index 055362dd..5cb9d6c3 100644 --- a/src/main/java/org/komunumo/ui/website/WebsiteHeader.java +++ b/src/main/java/org/komunumo/ui/website/WebsiteHeader.java @@ -18,7 +18,6 @@ package org.komunumo.ui.website; import com.vaadin.flow.component.html.Anchor; -import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Header; import org.jetbrains.annotations.NotNull; import org.komunumo.data.service.DatabaseService; @@ -27,10 +26,10 @@ public class WebsiteHeader extends Header { public WebsiteHeader(@NotNull final DatabaseService databaseService) { setId("website-header"); - final var configuration = databaseService.configuration(); + add( - new Anchor(configuration.getWebsiteBaseUrl(), - new H1(configuration.getWebsiteName())) + new Anchor("/", new WebsiteLogo(databaseService)), + new WebsiteStats() ); } diff --git a/src/main/java/org/komunumo/ui/website/WebsiteLayout.java b/src/main/java/org/komunumo/ui/website/WebsiteLayout.java index 5f890802..50db68af 100644 --- a/src/main/java/org/komunumo/ui/website/WebsiteLayout.java +++ b/src/main/java/org/komunumo/ui/website/WebsiteLayout.java @@ -23,15 +23,18 @@ import com.vaadin.flow.router.RouterLayout; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.komunumo.data.service.DatabaseService; public final class WebsiteLayout extends Div implements RouterLayout { private final Main main; - public WebsiteLayout() { + public WebsiteLayout(@NotNull final DatabaseService databaseService) { setId("website-container"); main = new Main(); + add(new WebsiteHeader(databaseService)); add(main); + add(new WebsiteFooter(databaseService)); } @Override diff --git a/src/main/java/org/komunumo/ui/website/WebsiteLogo.java b/src/main/java/org/komunumo/ui/website/WebsiteLogo.java new file mode 100644 index 00000000..93683531 --- /dev/null +++ b/src/main/java/org/komunumo/ui/website/WebsiteLogo.java @@ -0,0 +1,62 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.ui.website; + +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.server.InvalidApplicationConfigurationException; +import org.jetbrains.annotations.NotNull; +import org.komunumo.data.service.DatabaseService; + +import java.io.Serial; +import java.util.Random; + +public class WebsiteLogo extends Image { + + @Serial + private static final long serialVersionUID = 5073126350713287726L; + private final String logoUrlTemplate; + private final int minLogoNumber; + private final int maxLogoNumber; + private final boolean randomizeLogo; + + public WebsiteLogo(final @NotNull DatabaseService databaseService) { + final var configuration = databaseService.configuration(); + this.logoUrlTemplate = configuration.getWebsiteLogoTemplate(); + this.minLogoNumber = configuration.getWebsiteMinLogoNumber(); + this.maxLogoNumber = configuration.getWebsiteMaxLogoNumber(); + this.randomizeLogo = minLogoNumber != 0 || maxLogoNumber != 0; + + if (logoUrlTemplate == null || logoUrlTemplate.isBlank()) { + throw new InvalidApplicationConfigurationException("Missing website logo URL template!"); + } + + setAlt("Website Logo"); + setSrc(getLogoUrl()); + addClassName("website-logo"); + } + + private String getLogoUrl() { + return randomizeLogo ? getRandomLogoUrl() : logoUrlTemplate; + } + + private String getRandomLogoUrl() { + final var randomNumber = new Random().nextInt(maxLogoNumber - minLogoNumber) + minLogoNumber; + return String.format(logoUrlTemplate, randomNumber); + } + +} diff --git a/src/main/java/org/komunumo/ui/website/WebsiteStats.java b/src/main/java/org/komunumo/ui/website/WebsiteStats.java new file mode 100644 index 00000000..fc7be47e --- /dev/null +++ b/src/main/java/org/komunumo/ui/website/WebsiteStats.java @@ -0,0 +1,41 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.ui.website; + +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; + +import static org.komunumo.util.FormatterUtil.formatNumber; + +public class WebsiteStats extends Div { + + public WebsiteStats() { + final var stats = new Stats(8, "bits are a byte"); + final var number = new Span(new Text(formatNumber(stats.number()))); + number.addClassName("number"); + final var text = new Span(new Text(stats.text())); + text.addClassName("text"); + + add(number, text); + addClassName("website-stats"); + } + + private record Stats(int number, String text) { } + +} diff --git a/src/main/java/org/komunumo/ui/website/home/HomeView.java b/src/main/java/org/komunumo/ui/website/home/HomeView.java index 16f1fbc8..1a1bc653 100644 --- a/src/main/java/org/komunumo/ui/website/home/HomeView.java +++ b/src/main/java/org/komunumo/ui/website/home/HomeView.java @@ -18,20 +18,18 @@ package org.komunumo.ui.website.home; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; import com.vaadin.flow.router.Route; import com.vaadin.flow.server.auth.AnonymousAllowed; -import org.jetbrains.annotations.NotNull; -import org.komunumo.data.service.DatabaseService; -import org.komunumo.ui.website.WebsiteHeader; import org.komunumo.ui.website.WebsiteLayout; @Route(value = "", layout = WebsiteLayout.class) @AnonymousAllowed public class HomeView extends Div { - public HomeView(@NotNull final DatabaseService databaseService) { + public HomeView() { setId("home-view"); - add(new WebsiteHeader(databaseService)); + add(new H2("Home")); } } diff --git a/src/main/java/org/komunumo/util/FormatterUtil.java b/src/main/java/org/komunumo/util/FormatterUtil.java new file mode 100644 index 00000000..9f4702b3 --- /dev/null +++ b/src/main/java/org/komunumo/util/FormatterUtil.java @@ -0,0 +1,45 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.util; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +public final class FormatterUtil { + + private static final DecimalFormat LARGE_NUMBERS = createLargeNumberFormatter(); + + private static DecimalFormat createLargeNumberFormatter() { + final var formatter = (DecimalFormat) NumberFormat.getInstance(); + final var symbols = formatter.getDecimalFormatSymbols(); + + symbols.setGroupingSeparator('\''); + formatter.setDecimalFormatSymbols(symbols); + + return formatter; + } + + public static String formatNumber(final long number) { + return LARGE_NUMBERS.format(number); + } + + private FormatterUtil() { + throw new IllegalStateException("Utility class"); + } + +} diff --git a/src/main/resources/db/migration/V1_0_1__configuration.sql b/src/main/resources/db/migration/V1_0_1__configuration.sql index af7098bb..e612d66a 100644 --- a/src/main/resources/db/migration/V1_0_1__configuration.sql +++ b/src/main/resources/db/migration/V1_0_1__configuration.sql @@ -2,6 +2,14 @@ INSERT INTO `configuration` (`conf_key`, `conf_value`) VALUES ('website.url', 'http://localhost:8080'), - ('website.name', 'Java User Group Switzerland'); + ('website.favicon', 'https://static.jug.ch/images/favicon.ico'), + ('website.name', 'Java User Group Switzerland'), + ('website.contact.address', '8000 Zürich'), + ('website.contact.email', 'info@jug.ch'), + ('website.copyright', '© Java User Group Switzerland'), + ('website.about.text', '

JUG Switzerland aims at promoting the application of Java technology in Switzerland.

JUG Switzerland facilitates the sharing of experience and information among its members. This is accomplished through workshops, seminars and conferences. JUG Switzerland supports and encourages the cooperation between commercial organizations and research institutions.

JUG Switzerland is funded through membership fees and industry sponsors.

'), + ('website.logo.template', 'https://static.jug.ch/images/logos/jugs_logo_%02d.gif'), + ('website.logo.min', '1'), + ('website.logo.max', '22'); -- [jooq ignore stop] diff --git a/src/test/java/org/komunumo/ui/website/WebsiteLayoutTest.java b/src/test/java/org/komunumo/ui/website/WebsiteLayoutTest.java index ad7985e4..8207e082 100644 --- a/src/test/java/org/komunumo/ui/website/WebsiteLayoutTest.java +++ b/src/test/java/org/komunumo/ui/website/WebsiteLayoutTest.java @@ -17,20 +17,28 @@ */ package org.komunumo.ui.website; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.html.Paragraph; import org.junit.jupiter.api.Test; +import org.komunumo.ui.KaribuTestBase; +import org.komunumo.ui.website.home.HomeView; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -class WebsiteLayoutTest { +class WebsiteLayoutTest extends KaribuTestBase { @Test void testRouterLayoutContent() { - final var websiteLayout = new WebsiteLayout(); - assertEquals(1, websiteLayout.getComponentCount()); + UI.getCurrent().navigate(HomeView.class); + final var uiParent = UI.getCurrent().getCurrentView().getParent().orElseThrow() + .getParent().orElseThrow(); - final var main = websiteLayout.getComponentAt(0); - assertEquals(0, main.getElement().getChildCount()); + final var websiteLayout = (WebsiteLayout) uiParent; + assertEquals(3, websiteLayout.getComponentCount()); + + final var main = (Main) websiteLayout.getComponentAt(1); + assertEquals(1, main.getElement().getChildCount()); websiteLayout.showRouterLayoutContent(new Paragraph("foo")); assertEquals(1, main.getElement().getChildCount()); diff --git a/src/test/java/org/komunumo/ui/website/WebsiteLogoTest.java b/src/test/java/org/komunumo/ui/website/WebsiteLogoTest.java new file mode 100644 index 00000000..ecbda37b --- /dev/null +++ b/src/test/java/org/komunumo/ui/website/WebsiteLogoTest.java @@ -0,0 +1,91 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.ui.website; + +import com.vaadin.flow.server.InvalidApplicationConfigurationException; +import org.junit.jupiter.api.Test; +import org.komunumo.configuration.Configuration; +import org.komunumo.data.service.DatabaseService; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WebsiteLogoTest { + + @Test + void testRandomLogo() { + final var configuration = mock(Configuration.class); + when(configuration.getWebsiteLogoTemplate()).thenReturn("test_%02d.svg"); + when(configuration.getWebsiteMinLogoNumber()).thenReturn(1); + when(configuration.getWebsiteMaxLogoNumber()).thenReturn(5); + final var databaseService = mock(DatabaseService.class); + when(databaseService.configuration()).thenReturn(configuration); + + final var websiteLogo = new WebsiteLogo(databaseService); + assertEquals("website-logo", websiteLogo.getClassName()); + assertEquals("Website Logo", websiteLogo.getAlt().orElseThrow()); + assertTrue(websiteLogo.getSrc().matches("test_\\d{2}\\.svg")); + } + + @Test + void testStaticLogo() { + final var configuration = mock(Configuration.class); + when(configuration.getWebsiteLogoTemplate()).thenReturn("test.svg"); + when(configuration.getWebsiteMinLogoNumber()).thenReturn(0); + when(configuration.getWebsiteMaxLogoNumber()).thenReturn(0); + final var databaseService = mock(DatabaseService.class); + when(databaseService.configuration()).thenReturn(configuration); + + final var websiteLogo = new WebsiteLogo(databaseService); + assertEquals("website-logo", websiteLogo.getClassName()); + assertEquals("Website Logo", websiteLogo.getAlt().orElseThrow()); + assertTrue(websiteLogo.getSrc().matches("test\\.svg")); + } + + @Test + void testTemplateBlank() { + final var configuration = mock(Configuration.class); + when(configuration.getWebsiteLogoTemplate()).thenReturn(" "); + when(configuration.getWebsiteMinLogoNumber()).thenReturn(0); + when(configuration.getWebsiteMaxLogoNumber()).thenReturn(0); + final var databaseService = mock(DatabaseService.class); + when(databaseService.configuration()).thenReturn(configuration); + + final var exception = assertThrows(InvalidApplicationConfigurationException.class, + () -> new WebsiteLogo(databaseService)); + assertEquals("Missing website logo URL template!", exception.getMessage()); + } + + @Test + void testTemplateNull() { + final var configuration = mock(Configuration.class); + when(configuration.getWebsiteLogoTemplate()).thenReturn(null); + when(configuration.getWebsiteMinLogoNumber()).thenReturn(0); + when(configuration.getWebsiteMaxLogoNumber()).thenReturn(0); + final var databaseService = mock(DatabaseService.class); + when(databaseService.configuration()).thenReturn(configuration); + + final var exception = assertThrows(InvalidApplicationConfigurationException.class, + () -> new WebsiteLogo(databaseService)); + assertEquals("Missing website logo URL template!", exception.getMessage()); + } + +} diff --git a/src/test/java/org/komunumo/ui/website/home/HomeViewIT.java b/src/test/java/org/komunumo/ui/website/home/HomeViewIT.java index 54606630..a377e82b 100644 --- a/src/test/java/org/komunumo/ui/website/home/HomeViewIT.java +++ b/src/test/java/org/komunumo/ui/website/home/HomeViewIT.java @@ -18,7 +18,7 @@ package org.komunumo.ui.website.home; import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; import org.junit.jupiter.api.Test; import org.komunumo.ui.KaribuTestBase; @@ -30,8 +30,8 @@ class HomeViewIT extends KaribuTestBase { @Test void homeViewTest() { UI.getCurrent().navigate(HomeView.class); - final var title = _get(H1.class).getText(); - assertEquals("Java User Group Switzerland", title); + final var title = _get(H2.class, spec -> spec.withText("Home")).getText(); + assertEquals("Home", title); } } diff --git a/src/test/java/org/komunumo/util/FormatterUtilTest.java b/src/test/java/org/komunumo/util/FormatterUtilTest.java new file mode 100644 index 00000000..9d54e67b --- /dev/null +++ b/src/test/java/org/komunumo/util/FormatterUtilTest.java @@ -0,0 +1,59 @@ +/* + * Komunumo - Open Source Community Manager + * Copyright (C) Marcus Fihlon and the individual contributors to Komunumo. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.komunumo.util; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FormatterUtilTest { + + @Test + void testFormatNumber() { + assertEquals("1", FormatterUtil.formatNumber(1L)); + assertEquals("12", FormatterUtil.formatNumber(12L)); + assertEquals("123", FormatterUtil.formatNumber(123L)); + assertEquals("1'234", FormatterUtil.formatNumber(1234L)); + assertEquals("12'345", FormatterUtil.formatNumber(12345L)); + assertEquals("123'456", FormatterUtil.formatNumber(123456L)); + assertEquals("1'234'567", FormatterUtil.formatNumber(1234567L)); + assertEquals("12'345'678", FormatterUtil.formatNumber(12345678L)); + assertEquals("123'456'789", FormatterUtil.formatNumber(123456789L)); + } + + @Test + @SuppressWarnings("PMD.AvoidAccessibilityAlteration") // this is exactly what we want to test + void privateConstructorWithException() { + final var cause = assertThrows(InvocationTargetException.class, () -> { + Constructor constructor = FormatterUtil.class.getDeclaredConstructor(); + if (Modifier.isPrivate(constructor.getModifiers())) { + constructor.setAccessible(true); + constructor.newInstance(); + } + }).getCause(); + assertInstanceOf(IllegalStateException.class, cause); + assertEquals("Utility class", cause.getMessage()); + } + +}