diff --git a/pom.xml b/pom.xml index 05b4be926b..fa01cb36a4 100644 --- a/pom.xml +++ b/pom.xml @@ -172,6 +172,7 @@ wicketstuff-servlet3-examples wicketstuff-clipboardjs-examples wicketstuff-springreference-examples + wicketstuff-spring-boot-starter-examples jasperreports-examples editable-grid-examples wicketstuff-sitemap-xml-examples @@ -463,6 +464,7 @@ wicket-servlet3-parent wicketstuff-clipboard-js-parent wicketstuff-springreference-parent + wicketstuff-spring-boot-starter-parent wicket-security-parent yui-parent diff --git a/wicketstuff-bom/pom.xml b/wicketstuff-bom/pom.xml index 7f158b872f..24d104473c 100644 --- a/wicketstuff-bom/pom.xml +++ b/wicketstuff-bom/pom.xml @@ -239,6 +239,10 @@ org.wicketstuff wicketstuff-springreference-examples + + org.wicketstuff + wicketstuff-spring-boot-starter-examples + org.wicketstuff jasperreports-examples diff --git a/wicketstuff-spring-boot-starter-parent/LICENSE b/wicketstuff-spring-boot-starter-parent/LICENSE new file mode 100644 index 0000000000..9af2673380 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the + purposes of this License, Derivative Works shall not include works + that remain separable from, or merely link (or bind by name) to the + interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the + original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/wicketstuff-spring-boot-starter-parent/README.md b/wicketstuff-spring-boot-starter-parent/README.md new file mode 100644 index 0000000000..3b1bca765b --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/README.md @@ -0,0 +1,435 @@ +# Wicket Spring Boot Starter + +A lightweight, zero-boilerplate starter that integrates **Apache Wicket 10.x** with **Spring Boot 4.x**. It configures the Wicket web context automatically, bridges Spring injection into Wicket components, and enables simple configuration via `application.properties` or `application.yml`. + +--- + +## Motivation + +Running Wicket on Spring Boot always needs the same handful of pieces: a `WebApplication`, a filter +registration for it, and a `SpringComponentInjector` so that `@SpringBean` works. None of it is +difficult, but it is the same every time, it is easy to get subtly wrong, and it is the first thing +standing between someone and a running Wicket page. + +This starter supplies exactly those pieces and then gets out of the way. Everything past that point +is ordinary Wicket and ordinary Spring, documented in their own projects. + +**Staying small is the point, not a limitation.** A deliberately narrow starter is cheap to keep +alive: when Wicket or Spring Boot publishes a new major version, there is very little surface to +re-verify. It also lives inside `wicketstuff-core` and is released with it, so the version you need +is simply the one matching your Wicket version, and there is no second release cycle to wait for. + +The goal is also to make Apache Wicket selectable on [start.spring.io](https://start.spring.io), the +way Vaadin already is, so that a new Wicket project is one click away. See +[Road to start.spring.io](#road-to-startspringio) for the plan. + +### Is this the right starter for you? + +There is an established alternative, [`MarcGiffing/wicket-spring-boot`][giffing] +(`com.giffing.wicket.spring.boot.starter`), which is a much broader integration: Spring Security, +native WebSockets, bean validation, CSRF protection, several serializers, session datastores, +monitoring and a set of development-mode helpers. + +Pick that one if you want those batteries included. Pick this one if you would rather start from the +smallest thing that works and add what you need yourself. They solve the same problem with opposite +philosophies, and neither is a replacement for the other. + +[giffing]: https://github.com/MarcGiffing/wicket-spring-boot + +--- + +## Features + +- **Auto-Configuration**: Automatically registers Wicket's `WicketFilter` in Spring Boot's embedded servlet container. +- **Auto-Discovery**: Detects any subclass of Wicket's `WebApplication` registered as a Spring bean/component and binds it. +- **Spring Injection Support**: Automatically adds `SpringComponentInjector` to the application, allowing you to use `@SpringBean` to inject Spring-managed components directly into Wicket Pages and Panels. +- **Externalized Settings**: Configure Wicket's execution mode (`DEVELOPMENT` vs `DEPLOYMENT`) and the filter's path, name and order directly from Spring properties. +- **IDE Auto-Completion Support**: Ships with built-in configuration metadata, enabling auto-completion and documentation tooltips for Wicket properties in popular IDEs (IntelliJ, Eclipse, VS Code). +- **Spring MVC Coexistence**: Requests Wicket does not handle fall through to Spring MVC, so `@RestController` endpoints work next to Wicket pages. +- **DevTools Support**: Works with `spring-boot-devtools` out of the box: automatic restarts, the back button and restored pages keep working with `@SpringBean` fields. + +--- + +## How It Works + +The starter contributes a single auto-configuration, `WicketAutoConfiguration`. It applies only when +all of the following hold: + +- the application is a **servlet** web application (a reactive application is left alone); +- Wicket's `WebApplication` and `WicketFilter` are on the classpath; +- `wicket.enabled` is not set to `false`. + +When it applies, it contributes: + +| Bean | What it does | +|---|---| +| `webApplication` | The Wicket `WebApplication`. Falls back to a built-in default (see below) if you have not defined one. | +| `springComponentInjectorRegistrar` | Adds Wicket's `SpringComponentInjector` to every `WebApplication` bean as soon as it is created, which is what makes `@SpringBean` work inside pages and components, and already in your application's `init()`. | +| `wicketFilterRegistration` | Registers `WicketFilter` with the servlet container, mapped at `wicket.filter-path`, passing Wicket's filter-mapping and `configuration` init parameters from your settings. | + +### Living alongside Spring MVC + +The starter brings `spring-boot-starter-webmvc`, so your application also has Spring MVC available. +Wicket's filter is mapped at `/*` but forwards anything it does not handle further down the filter +chain, so `@RestController` endpoints keep working next to Wicket pages: + +```java +@RestController +class GreetingController { + + @GetMapping("/api/greeting") + String greeting() { + return "hello"; + } +} +``` + +With the defaults, `/api/greeting` reaches the controller while `/` renders your Wicket home page. +If you would rather keep the two strictly apart, confine Wicket to its own prefix with +`wicket.filter-path=/app/*`. + +### Deploying as a WAR + +The starter brings an embedded Tomcat, but it does not commit you to it. Spring Boot's usual recipe +for deploying to an external servlet container works unchanged — set `war` packaging, extend +`SpringBootServletInitializer`, and re-declare the container at `provided` scope: + +```xml + + org.springframework.boot + spring-boot-starter-tomcat + provided + +``` + +A direct declaration wins over the transitive one, so the embedded container is demoted to +`provided` and stays out of `WEB-INF/lib` while Wicket and the starter are packaged as normal. + +### Using Spring Boot DevTools + +The starter works with `spring-boot-devtools` out of the box. It ships a +`META-INF/spring-devtools.properties` that loads the Wicket and wicketstuff jars in DevTools' +restart classloader, next to your own classes. Without it, Wicket would restore stored pages +(e.g. on the back button) against a stale copy of your classes and fail with a +`ClassCastException` on every `@SpringBean` field. + +On a restart your session survives, but by default the pages in it do not: Wicket keeps them in +the servlet container's temporary directory, and Spring Boot creates a new one for every start of +the embedded Tomcat. Requesting an old page then simply renders it afresh. To keep page state across +restarts as well, give Tomcat a fixed base directory in development: + +```properties +server.tomcat.basedir=target/tomcat +``` + +--- + +## Dependency Configuration + +This is the only dependency you need: it brings Apache Wicket, the Wicket/Spring bridge and the +embedded servlet container with it. Add it to your Maven `pom.xml` (check +[Maven Central](https://central.sonatype.com/artifact/org.wicketstuff/wicketstuff-spring-boot-starter) +for the latest released version): + +```xml + + org.wicketstuff + wicketstuff-spring-boot-starter + 10.12.0 + +``` + +If your build already imports the `wicketstuff-bom`, the version can be left out. + +--- + +## Compatibility Matrix + +Ensure you match the correct starter version with your Spring Boot and Java environment: + +| WicketStuff / Starter Version | Spring Boot Version | Spring Framework Version | Minimum Java Version | +| :--- | :--- | :--- | :--- | +| **`10.12.x`** (Current) | `4.0.x`, `4.1.x` | `7.0.x` | Java 17 | + +Compatibility is checked by the integration test under `wicketstuff-spring-boot-starter/src/it`: a +project shaped like one generated by Spring Initializr, inheriting from `spring-boot-starter-parent` +so it runs against exactly the library versions Spring Boot manages. Every build runs it against the +Spring Boot version in the parent pom; to check another supported line, run from the starter module: + +```bash +mvn verify -Dspring-boot.version=4.0.8 +``` + +--- + +## The Default Page + +Add the dependency, start the application, and you already have a running Wicket application: with +no `WebApplication` bean of your own, the starter registers a built-in one that serves a placeholder +home page telling you how to replace it. + +That default disappears the moment you declare your own `WebApplication` bean, which is what the +Quickstart below does. It exists so a freshly generated project runs and shows something, not as a +page you are meant to keep. + +--- + +## Quickstart + +### 1. Create a Wicket Application Component +Subclass `WebApplication` and register it as a Spring bean using `@Component`: + +```java +package com.example; + +import org.apache.wicket.Page; +import org.apache.wicket.protocol.http.WebApplication; +import org.springframework.stereotype.Component; + +@Component +public class WicketApplication extends WebApplication { + + @Override + public Class getHomePage() { + return HomePage.class; + } + + @Override + protected void init() { + super.init(); + // Additional custom Wicket configurations... + } +} +``` + +### 2. Inject Spring Beans into Pages +Any Spring bean can be injected, for example a simple service: + +```java +package com.example; + +import org.springframework.stereotype.Service; + +@Service +public class MySpringService { + + public String sayHello() { + return "Hello from Spring!"; + } +} +``` + +Use Wicket's standard `@SpringBean` annotation to inject it into a page: + +```java +package com.example; + +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.spring.injection.annot.SpringBean; + +public class HomePage extends WebPage { + private static final long serialVersionUID = 1L; + + @SpringBean + private MySpringService mySpringService; + + public HomePage() { + add(new Label("message", mySpringService.sayHello())); + } +} +``` + +Every Wicket page needs its markup, in an HTML file named after the class. Put it in +`src/main/resources/com/example/HomePage.html`: Maven only copies resources from +`src/main/resources`, so an HTML file next to the Java source would not end up on the classpath. + +```html + + + +

+ + +``` + +### 3. Bootstrap your Spring Boot App +Annotate your entry point with `@SpringBootApplication` and run it: + +```java +package com.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} +``` + +--- + +## Overriding the Defaults + +Every bean the starter contributes steps aside as soon as you define your own, so you can take over +as much or as little as you need: + +| Bean | Steps aside when | Define your own to | +|---|---|---| +| `webApplication` | any `WebApplication` bean exists | use your own Wicket application (the usual case — see the Quickstart) | +| `springComponentInjectorRegistrar` | any `SpringComponentInjector` bean exists | control how Spring injection is wired | +| `wicketFilterRegistration` | a `FilterRegistrationBean` or a plain `WicketFilter` bean exists | control every detail of the filter registration (for path, name and order, the `wicket.filter-*` properties are enough) | + +Overriding the filter registration does **not** cost you Spring injection: the injector is bound to +the `WebApplication` rather than to the filter, so `@SpringBean` keeps working in your pages either +way. + +### Migrating an existing Wicket + Spring application + +Applications that wire Wicket and Spring by hand usually register the injector themselves, along +these lines: + +```java +@Override +protected void init() { + super.init(); + getComponentInstantiationListeners().add(new SpringComponentInjector(this)); +} +``` + +Keeping that line as well as the starter's registration would register two injectors. Either drop +it and let the starter do it, or, if you want to keep control of the wiring, declare your own +`SpringComponentInjector` bean so the starter steps aside. + +--- + +## Configuration Properties + +The following properties can be configured in your `application.properties` or `application.yml` file: + +| Property | Default Value | Description | +|---|---|---| +| `wicket.enabled` | `true` | Set to `false` to switch the auto-configuration off entirely. | +| `wicket.filter-path` | `/*` | URL mapping pattern for the Wicket filter. | +| `wicket.filter-name` | `wicket-filter` | The name of the registered Wicket servlet filter. | +| `wicket.filter-order` | `Ordered.LOWEST_PRECEDENCE` | Position of the Wicket filter in the servlet filter chain; lower values run earlier. | +| `wicket.configuration` | `DEVELOPMENT` | The configuration type: `DEVELOPMENT` or `DEPLOYMENT`. | + +For example: +```properties +wicket.configuration=DEPLOYMENT +wicket.filter-path=/app/* +wicket.filter-name=my-custom-wicket-filter +``` + +By default the Wicket filter runs last, after every other filter, including Spring Security's +filter chain (order `-100`). That is usually what you want, since security decisions are made before +Wicket renders anything. Set `wicket.filter-order` if another filter of yours has to run after +Wicket instead. + +--- + +## Going to Production + +Like Wicket itself, the starter runs in `DEVELOPMENT` mode unless told otherwise, and says so with a +banner in the log on every start. That mode is meant for your machine only: it reloads changed +markup, leaves `wicket:id` attributes in the rendered HTML, enables the Ajax debug window, serves +unminified JavaScript and renders exception pages with full stack traces. + +Switch to `DEPLOYMENT` wherever the application actually runs. A Spring profile keeps the setting +next to the rest of your production configuration, in `application-prod.properties`: + +```properties +wicket.configuration=DEPLOYMENT +``` + +activated with `--spring.profiles.active=prod` (or `SPRING_PROFILES_ACTIVE=prod`). Without a +profile, setting the environment variable `WICKET_CONFIGURATION=DEPLOYMENT` does the same. + +--- + +## Testing + +Wicket keys its application registry by the filter name, and Spring keeps test contexts cached for +the lifetime of the JVM. Two tests that each start a real servlet container therefore collide on the +default filter name, failing with `Application with name 'wicket-filter' already exists`. + +Give each such test its own filter name: + +```java +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "wicket.filter-name=my-test-filter") +class MyIntegrationTest { + // ... +} +``` + +Tests that do not start a container (the default `MOCK` web environment) are unaffected, because the +filter is never initialised. + +--- + +## Spring Initializr Metadata Registration + +### Road to start.spring.io + +Listing the starter on start.spring.io is planned once it has been released, in three steps: + +1. The starter is merged into `wicketstuff-core`. +2. It ships with the next wicketstuff release and is published to Maven Central. The Initializr can + only reference released versions, never snapshots. +3. The listing is requested from the Spring Initializr team with the entry below, and kept up to + date for every new Spring Boot line afterwards. + +Until then, add the dependency to a generated Spring Boot project by hand, as shown in +[Dependency Configuration](#dependency-configuration). + +### The Initializr entry + +To list the starter on `start.spring.io` (or in an internal Initializr instance), the dependency is +added to the Initializr's `application.yml`. Spring Boot's BOM does not manage `org.wicketstuff` +artifacts, so the entry has to say which starter version goes with which Spring Boot line; +otherwise the generated build has no version for the dependency and does not resolve. + +```yaml +initializr: + dependencies: + - name: Web + content: + - name: Apache Wicket + id: wicket + description: Build component-oriented, server-side web applications with Apache Wicket. + groupId: org.wicketstuff + artifactId: wicketstuff-spring-boot-starter + compatibilityRange: "[4.0.0,4.2.0-M1)" + mappings: + - compatibilityRange: "[4.0.0,4.2.0-M1)" + version: 10.12.0 + links: + - rel: reference + href: https://github.com/wicketstuff/core/tree/master/wicketstuff-spring-boot-starter-parent + - rel: guide + href: https://wicket.apache.org/start/quickstart.html +``` + +- `compatibilityRange` is always bounded above. When a new Spring Boot line appears, it is only + opened up once the integration test above passes against it, so that Initializr never offers a + combination that has not been tested. +- Each `mappings` entry pins the starter release for a Spring Boot range. When a starter release + drops or adds a Spring Boot line, split the range into several mappings. +- `version` must be a release available on Maven Central, never a snapshot. + +--- + +## Further Reading + +- [Apache Wicket documentation](https://wicket.apache.org/) — writing pages, components and markup. +- [WicketStuff wiki](https://github.com/wicketstuff/core/wiki) — documentation for the other modules + in this project. +- [API documentation](https://www.javadoc.io/doc/org.wicketstuff/wicketstuff-spring-boot-starter) — the + starter's own Javadoc, published per release. diff --git a/wicketstuff-spring-boot-starter-parent/pom.xml b/wicketstuff-spring-boot-starter-parent/pom.xml new file mode 100644 index 0000000000..b4206d376a --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + + org.wicketstuff + wicketstuff-core + 10.12.0-SNAPSHOT + + + wicketstuff-spring-boot-starter-parent + pom + + WicketStuff Spring Boot Starter - Parent + + Parent project for Wicket Spring Boot Starter module and its example application. + + + + 4.1.1 + + + 7.0.9 + 3.1.5 + 2.21.5 + 2.0.18 + 1.5.38 + 2.25.5 + 1.3.6 + 6.0.3 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + wicketstuff-spring-boot-starter + wicketstuff-spring-boot-starter-examples + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/README.md b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/README.md new file mode 100644 index 0000000000..524710b7dd --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/README.md @@ -0,0 +1,47 @@ +# WicketStuff Spring Boot Starter — Examples + +A runnable example for the starter, plus the integration tests that demonstrate and verify its +documented behaviour. + +## Running it + +From the repository root, install the starter first, then run the example: + +```bash +mvn -pl wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter -am install -DskipTests +mvn -f wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples spring-boot:run +``` + +Then open . + +`ExampleApplication` contains nothing but a Spring Boot entry point — no `WebApplication`, no page, +no Wicket configuration. That is deliberate: it shows what the starter gives you on its own, which +is a working Wicket application serving the built-in placeholder page. + +`src/main/resources/application.properties` overrides a couple of `wicket.*` settings purely to +show that externalised configuration reaches Wicket; none of it is required. + +## Where each pattern is demonstrated + +The patterns you would write in a real application live in `src/test/java`, so they are exercised on +every build rather than drifting out of date as untested sample code: + +| What you want to see | Where | +|---|---| +| A custom `WebApplication` and a page using `@SpringBean` | `SpringBeanInjectionQuickstartTest` (with `QuickstartWebApplication`, `QuickstartHomePage`, `GreetingService`) | +| That `@SpringBean` still works if you supply your own filter registration | `SpringBeanInjectionWithCustomFilterRegistrationTest` | +| That `@SpringBean` already works inside your application's `init()` | `SpringBeanInjectionDuringInitTest` | +| REST controllers living alongside Wicket pages | `WicketAndSpringMvcCoexistenceTest` | +| `wicket.*` properties actually reaching the filter | `WicketPropertiesBindingTest` | +| That the starter alone is enough to boot a web application | `StarterSelfSufficiencyTest` | + +## A note on the dependencies + +This module depends on **nothing but the starter** at compile scope, and an enforcer rule in its +`pom.xml` keeps it that way. That is what makes `StarterSelfSufficiencyTest` meaningful: the +embedded servlet container it boots can only have come from the starter. If you need another +dependency here, it almost certainly belongs in the starter instead. + +Note also that each test which starts a real servlet container sets its own `wicket.filter-name`. +Wicket keys its application registry by that name and Spring caches test contexts, so tests would +otherwise collide — see the Testing section of the [starter README](../README.md). diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/pom.xml b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/pom.xml new file mode 100644 index 0000000000..5c078f0c29 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/pom.xml @@ -0,0 +1,110 @@ + + + + 4.0.0 + + + org.wicketstuff + wicketstuff-spring-boot-starter-parent + 10.12.0-SNAPSHOT + ../pom.xml + + + wicketstuff-spring-boot-starter-examples + jar + + WicketStuff Spring Boot Starter - Examples + + Runnable example for the WicketStuff Spring Boot Starter. The application itself is + deliberately empty, showing what the starter gives you with no configuration at all; the + integration tests alongside it demonstrate and verify the documented patterns, from + injecting Spring beans into pages to overriding the filter registration. + + + + + + org.wicketstuff + wicketstuff-spring-boot-starter + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-resttestclient + test + + + + org.springframework.boot + spring-boot-restclient + test + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-starter-is-the-only-compile-dependency + + enforce + + + + + false + + org.springframework.boot:spring-boot-starter-* + + + org.springframework.boot:spring-boot-starter-*:*:*:test + + + This example must depend on nothing but wicketstuff-spring-boot-starter at + compile scope, so that StarterSelfSufficiencyTest proves the starter + brings a working web application on its own. Declaring a Spring Boot + starter here would satisfy that test without the starter supplying + anything. Add the dependency to the starter instead. + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/java/org/wicketstuff/springboot/starter/example/ExampleApplication.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/java/org/wicketstuff/springboot/starter/example/ExampleApplication.java new file mode 100644 index 0000000000..2ca10f52bd --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/java/org/wicketstuff/springboot/starter/example/ExampleApplication.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Runnable example for the starter. + *

+ * There is deliberately nothing here beyond the Spring Boot entry point: no {@code WebApplication}, + * no page, no Wicket configuration. That is the point of the example — run this class and the + * starter alone gives you a working Wicket application on + * http://localhost:8080, serving its built-in placeholder page. + *

+ *

+ * The patterns you would actually write in your own application are demonstrated by the + * integration tests in {@code src/test/java}, where they are also verified on every build. See this + * module's README for a map of which test shows what. + *

+ * + * @author Daniel Bartl + */ +@SpringBootApplication +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/resources/application.properties b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/resources/application.properties new file mode 100644 index 0000000000..2d28e4f58a --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/main/resources/application.properties @@ -0,0 +1,13 @@ +# None of these are required -- the starter works with no configuration at all. They are set here +# to show that externalised wicket.* properties reach Wicket, and are asserted by +# WicketPropertiesBindingTest. +# +# DEVELOPMENT is Wicket's default and enables its development-time checks; switch to DEPLOYMENT in +# production, typically via a Spring profile. +wicket.configuration=DEVELOPMENT +# /* is also the default; shown here to make the mapping explicit. +wicket.filter-path=/* +wicket.filter-name=wicket-example-filter + +# Disable banner for cleaner startup logs +spring.main.banner-mode=off diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/DefaultWebApplicationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/DefaultWebApplicationTest.java new file mode 100644 index 0000000000..bd9ec21ce6 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/DefaultWebApplicationTest.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.protocol.http.WebApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.wicketstuff.springboot.starter.DefaultWebApplication; +import org.wicketstuff.springboot.starter.WicketAutoConfiguration; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the starter auto-configures {@link DefaultWebApplication} + * when no custom {@link WebApplication} bean is present in the application context. + * + * @author Daniel Bartl + */ +@SpringBootTest(classes = DefaultWebApplicationTest.TestConfig.class) +public class DefaultWebApplicationTest { + + @Configuration + @ImportAutoConfiguration(WicketAutoConfiguration.class) + static class TestConfig { + } + + @Autowired + private WebApplication webApplication; + + @Test + public void testDefaultWebApplicationIsLoaded() { + assertNotNull(webApplication); + assertTrue(webApplication instanceof DefaultWebApplication); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/ExampleApplicationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/ExampleApplicationTest.java new file mode 100644 index 0000000000..277172b100 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/ExampleApplicationTest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.protocol.http.WebApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Basic Spring Boot context integration test. + * + * @author Daniel Bartl + */ +@SpringBootTest +public class ExampleApplicationTest { + + @Autowired + private WebApplication webApplication; + + @Test + public void contextLoads() { + assertNotNull(webApplication); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/GreetingService.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/GreetingService.java new file mode 100644 index 0000000000..83ef9d8af3 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/GreetingService.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +/** + * @author Daniel Bartl + */ +interface GreetingService { + + String greet(); +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartHomePage.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartHomePage.java new file mode 100644 index 0000000000..ab90ef1fe3 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartHomePage.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.spring.injection.annot.SpringBean; + +/** + * Mirrors the custom {@code HomePage} from the README's Quickstart section: a Wicket page + * that has a Spring-managed service injected via {@code @SpringBean}. + * + * @author Daniel Bartl + */ +public class QuickstartHomePage extends WebPage { + private static final long serialVersionUID = 1L; + + @SpringBean + private GreetingService greetingService; + + public QuickstartHomePage() { + add(new Label("message", greetingService.greet())); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartWebApplication.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartWebApplication.java new file mode 100644 index 0000000000..10a62e421d --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/QuickstartWebApplication.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.Page; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * Mirrors the custom {@code WebApplication} from the README's Quickstart section: a + * hand-written subclass that the starter's {@code @ConditionalOnMissingBean} default backs + * off for, once it is registered as a Spring bean. + * + * @author Daniel Bartl + */ +class QuickstartWebApplication extends WebApplication { + + @Override + public Class getHomePage() { + return QuickstartHomePage.class; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionDuringInitTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionDuringInitTest.java new file mode 100644 index 0000000000..998af058b7 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionDuringInitTest.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.injection.Injector; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.spring.injection.annot.SpringBean; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Spring Boot initialises the embedded container's filters, and with them the Wicket application, + * while the application context is still refreshing. Spring injection must already be in place at + * that point, so that {@link WebApplication#init()} can rely on {@code @SpringBean}, for example to + * inject a non-component object through {@link Injector#get()}. + * + * @author Daniel Bartl + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "wicket.filter-name=injection-during-init-filter") +@Import(SpringBeanInjectionDuringInitTest.InitInjectionConfig.class) +class SpringBeanInjectionDuringInitTest { + + @Autowired + private InitInjectingWebApplication application; + + @Test + void springBeansCanBeInjectedFromTheApplicationsInit() { + assertThat(application.greetingSeenDuringInit).isEqualTo("Hello from Spring, Wicket!"); + } + + @TestConfiguration + static class InitInjectionConfig { + + @Bean + InitInjectingWebApplication webApplication() { + return new InitInjectingWebApplication(); + } + + @Bean + GreetingService greetingService() { + return () -> "Hello from Spring, Wicket!"; + } + } + + static class InitInjectingWebApplication extends QuickstartWebApplication { + + private String greetingSeenDuringInit; + + @Override + protected void init() { + super.init(); + + GreetingHolder holder = new GreetingHolder(); + Injector.get().inject(holder); + greetingSeenDuringInit = holder.greetingService.greet(); + } + } + + static class GreetingHolder { + + @SpringBean + private GreetingService greetingService; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionQuickstartTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionQuickstartTest.java new file mode 100644 index 0000000000..06c675c5d5 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionQuickstartTest.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.protocol.http.WebApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * End-to-end proof of the README's Quickstart pattern: registering a custom + * {@link WebApplication} bean makes the starter's {@code @ConditionalOnMissingBean} default + * back off, and {@code @SpringBean} injects a Spring-managed service into the page that gets + * rendered through a real embedded servlet container. + * + * @author Daniel Bartl + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestRestTemplate +@Import(SpringBeanInjectionQuickstartTest.QuickstartConfig.class) +class SpringBeanInjectionQuickstartTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void rendersCustomHomePageWithTheSpringInjectedGreeting() { + String body = restTemplate.getForObject("/", String.class); + + assertThat(body).contains("Hello from Spring, Wicket!"); + } + + @TestConfiguration + static class QuickstartConfig { + + @Bean + WebApplication webApplication() { + return new QuickstartWebApplication(); + } + + @Bean + GreetingService greetingService() { + return () -> "Hello from Spring, Wicket!"; + } + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionWithCustomFilterRegistrationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionWithCustomFilterRegistrationTest.java new file mode 100644 index 0000000000..9ef1b573c3 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/SpringBeanInjectionWithCustomFilterRegistrationTest.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.WicketFilter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * An application that supplies its own {@link FilterRegistrationBean} makes the starter's + * registration back off. Spring injection must keep working regardless, which is why the + * {@code SpringComponentInjector} is bound to the {@link WebApplication} bean rather than to the + * filter registration. + * + * @author Daniel Bartl + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestRestTemplate +@Import(SpringBeanInjectionWithCustomFilterRegistrationTest.CustomRegistrationConfig.class) +class SpringBeanInjectionWithCustomFilterRegistrationTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void stillInjectsSpringBeansWhenTheFilterRegistrationIsOverridden() { + ResponseEntity response = restTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("Hello from Spring, Wicket!"); + } + + @TestConfiguration + static class CustomRegistrationConfig { + + @Bean + WebApplication webApplication() { + return new QuickstartWebApplication(); + } + + @Bean + GreetingService greetingService() { + return () -> "Hello from Spring, Wicket!"; + } + + @Bean + FilterRegistrationBean customWicketFilterRegistration(WebApplication webApplication) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new WicketFilter(webApplication)); + registration.addUrlPatterns("/*"); + registration.setName("custom-wicket-filter"); + registration.addInitParameter(WicketFilter.FILTER_MAPPING_PARAM, "/*"); + registration.addInitParameter("configuration", "DEVELOPMENT"); + return registration; + } + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/StarterSelfSufficiencyTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/StarterSelfSufficiencyTest.java new file mode 100644 index 0000000000..414ec0ba8f --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/StarterSelfSufficiencyTest.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Guards the contract that {@code wicketstuff-spring-boot-starter} is the only dependency an + * application needs. + *

+ * This module deliberately declares nothing but the starter at compile scope, so the embedded + * servlet container exercised here can only have arrived transitively from the starter itself. + * Without that, a project generated by Spring Initializr with just "Wicket" selected boots as a + * plain non-web application and silently serves nothing. + *

+ *

+ * A {@code bannedDependencies} enforcer rule in this module's pom keeps that precondition true: + * declaring a Spring Boot starter here at compile scope would make this test pass without the + * starter supplying anything. + *

+ *

+ * The filter name is overridden because Wicket keys its application registry by it, so two tests + * that each start a real container in the same JVM collide on the default name. + *

+ * + * @author Daniel Bartl + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "wicket.filter-name=self-sufficiency-filter") +@AutoConfigureTestRestTemplate +class StarterSelfSufficiencyTest { + + @Autowired + private ApplicationContext context; + + @Autowired + private TestRestTemplate restTemplate; + + /** + * Asserted on the class name rather than the type so that this class does not itself need a + * servlet-only import, which would make it fail to compile rather than fail as a test. + */ + @Test + void theStarterAloneBootsAServletWebApplication() { + assertThat(context.getClass().getName()).endsWith("ServletWebServerApplicationContext"); + } + + @Test + void theStarterAloneServesTheDefaultHomePage() { + ResponseEntity response = restTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("Wicket Spring Boot Starter"); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketAndSpringMvcCoexistenceTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketAndSpringMvcCoexistenceTest.java new file mode 100644 index 0000000000..4b321fd7b3 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketAndSpringMvcCoexistenceTest.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The starter depends on {@code spring-boot-starter-web}, so every application also has Spring + * MVC's {@code DispatcherServlet} while Wicket's filter is mapped at {@code /*}. This pins down + * that the two coexist: Wicket serves its pages and passes anything it does not handle further + * down the filter chain, so {@code @RestController} endpoints stay reachable. + * + * @author Daniel Bartl + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "wicket.filter-name=mvc-coexistence-filter") +@AutoConfigureTestRestTemplate +@Import(WicketAndSpringMvcCoexistenceTest.GreetingController.class) +class WicketAndSpringMvcCoexistenceTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void springMvcEndpointsRemainReachableBehindTheWicketFilter() { + ResponseEntity response = restTemplate.getForEntity("/api/greeting", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("hello from spring mvc"); + } + + @Test + void wicketStillServesItsOwnPages() { + ResponseEntity response = restTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("Wicket Spring Boot Starter"); + } + + @RestController + static class GreetingController { + + @GetMapping("/api/greeting") + String greeting() { + return "hello from spring mvc"; + } + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketPropertiesBindingTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketPropertiesBindingTest.java new file mode 100644 index 0000000000..e86ab927f2 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/java/org/wicketstuff/springboot/starter/example/WicketPropertiesBindingTest.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter.example; + +import org.apache.wicket.RuntimeConfigurationType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.wicketstuff.springboot.starter.WicketProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that the {@code wicket.*} properties set in this example's + * {@code application.properties} are actually bound and applied to the registered Wicket + * filter, rather than the starter's built-in defaults. + * + * @author Daniel Bartl + */ +@SpringBootTest +class WicketPropertiesBindingTest { + + @Autowired + private WicketProperties wicketProperties; + + @Autowired + private FilterRegistrationBean wicketFilterRegistration; + + @Test + void bindsWicketPropertiesFromApplicationProperties() { + assertThat(wicketProperties.getFilterPath()).isEqualTo("/*"); + assertThat(wicketProperties.getFilterName()).isEqualTo("wicket-example-filter"); + assertThat(wicketProperties.getConfiguration()).isEqualTo(RuntimeConfigurationType.DEVELOPMENT); + } + + @Test + void appliesThePropertiesToTheRegisteredWicketFilter() { + assertThat(wicketFilterRegistration.getFilterName()).isEqualTo("wicket-example-filter"); + assertThat(wicketFilterRegistration.getUrlPatterns()).containsExactly("/*"); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/resources/org/wicketstuff/springboot/starter/example/QuickstartHomePage.html b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/resources/org/wicketstuff/springboot/starter/example/QuickstartHomePage.html new file mode 100644 index 0000000000..d378b97d2b --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter-examples/src/test/resources/org/wicketstuff/springboot/starter/example/QuickstartHomePage.html @@ -0,0 +1,18 @@ + + + +placeholder + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/pom.xml b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..9c8b91010d --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + + org.wicketstuff + wicketstuff-spring-boot-starter-parent + 10.12.0-SNAPSHOT + ../pom.xml + + + wicketstuff-spring-boot-starter + jar + + WicketStuff Spring Boot Starter + + Lightweight auto-configuration and integration starter for Apache Wicket and Spring Boot. + + + + 3.10.1 + + false + + + + + + org.apache.wicket + wicket-core + + + org.apache.wicket + wicket-spring + + + org.springframework + spring-core + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-web + + + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.wicket + wicket-tester + test + + + + org.springframework + spring-web + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${settings.localRepository}/org/springframework/boot/spring-boot-dependencies/${spring-boot.version}/spring-boot-dependencies-${spring-boot.version}.pom + ${spring.version} + ${jackson.version} + ${jackson2.version} + ${slf4j.version} + ${logback.version} + ${log4j.version} + ${commons-logging.version} + ${junit.version} + + + + + + org.apache.maven.plugins + maven-invoker-plugin + ${maven-invoker-plugin.version} + + ${project.build.directory}/it + ${project.build.directory}/local-repo + src/it/settings.xml + + */pom.xml + + + verify + + + + ${spring-boot.version} + + true + ${skipTests} + + + + integration-test + + install + run + + + + + + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/pom.xml b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/pom.xml new file mode 100644 index 0000000000..a00de2e79b --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/pom.xml @@ -0,0 +1,35 @@ + + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + @spring-boot.version@ + + + + com.example + boot-managed-versions + 0.0.1-SNAPSHOT + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoApplication.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoApplication.java new file mode 100644 index 0000000000..1aefff2111 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoApplication.java @@ -0,0 +1,12 @@ +package com.example.wicket; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoWicketApplication.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoWicketApplication.java new file mode 100644 index 0000000000..6f7b84b843 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/DemoWicketApplication.java @@ -0,0 +1,14 @@ +package com.example.wicket; + +import org.apache.wicket.Page; +import org.apache.wicket.protocol.http.WebApplication; +import org.springframework.stereotype.Component; + +@Component +public class DemoWicketApplication extends WebApplication { + + @Override + public Class getHomePage() { + return HomePage.class; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/GreetingService.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/GreetingService.java new file mode 100644 index 0000000000..1873411a94 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/GreetingService.java @@ -0,0 +1,11 @@ +package com.example.wicket; + +import org.springframework.stereotype.Service; + +@Service +public class GreetingService { + + public String greet() { + return "Hello from a Spring bean"; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/HomePage.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/HomePage.java new file mode 100644 index 0000000000..2fe653ff91 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/HomePage.java @@ -0,0 +1,17 @@ +package com.example.wicket; + +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.spring.injection.annot.SpringBean; + +public class HomePage extends WebPage { + + private static final long serialVersionUID = 1L; + + @SpringBean + private GreetingService greetingService; + + public HomePage() { + add(new Label("greeting", greetingService.greet())); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/PingController.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/PingController.java new file mode 100644 index 0000000000..5077e3395d --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/java/com/example/wicket/PingController.java @@ -0,0 +1,13 @@ +package com.example.wicket; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PingController { + + @GetMapping("/api/ping") + public String ping() { + return "pong"; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/resources/com/example/wicket/HomePage.html b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/resources/com/example/wicket/HomePage.html new file mode 100644 index 0000000000..f78405b8f6 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/main/resources/com/example/wicket/HomePage.html @@ -0,0 +1,6 @@ + + + +

+ + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/test/java/com/example/wicket/DemoApplicationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/test/java/com/example/wicket/DemoApplicationTest.java new file mode 100644 index 0000000000..608d80fb68 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/boot-managed-versions/src/test/java/com/example/wicket/DemoApplicationTest.java @@ -0,0 +1,47 @@ +package com.example.wicket; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Boots the application in a real servlet container, with every library at the version Spring + * Boot manages, and exercises the starter end to end: the user's WebApplication is picked up, + * {@code @SpringBean} injection works in pages, and Spring MVC still answers behind the Wicket + * filter. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class DemoApplicationTest { + + @LocalServerPort + private int port; + + @Test + void rendersAWicketPageWithAnInjectedSpringBean() throws Exception { + HttpResponse response = get("/"); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).contains("Hello from a Spring bean"); + } + + @Test + void springMvcAnswersBehindTheWicketFilter() throws Exception { + HttpResponse response = get("/api/ping"); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).isEqualTo("pong"); + } + + private HttpResponse get(String path) throws Exception { + HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + return client.send(HttpRequest.newBuilder(URI.create("http://localhost:" + port + path)).build(), + HttpResponse.BodyHandlers.ofString()); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/settings.xml b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/settings.xml new file mode 100644 index 0000000000..54edb774f7 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/it/settings.xml @@ -0,0 +1,38 @@ + + + + + + it-repo + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + + it-repo + + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultHomePage.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultHomePage.java new file mode 100644 index 0000000000..5cedd3bda1 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultHomePage.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.markup.html.basic.Label; + +import java.io.Serial; + +/** + * The default home page loaded by the Wicket Spring Boot Starter + * when the application provides no custom WebApplication bean. + * + * @author Daniel Bartl + */ +public class DefaultHomePage extends WebPage { + @Serial + private static final long serialVersionUID = 1L; + + public DefaultHomePage() { + add(new Label("version", getApplication().getFrameworkSettings().getVersion())); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultWebApplication.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultWebApplication.java new file mode 100644 index 0000000000..18056f5e39 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/DefaultWebApplication.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.Page; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * The default {@link WebApplication} used by the Wicket Spring Boot Starter + * when the user does not provide their own custom implementation bean. + * + * @author Daniel Bartl + */ +public class DefaultWebApplication extends WebApplication { + + @Override + public Class getHomePage() { + return DefaultHomePage.class; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/SpringComponentInjectorRegistrar.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/SpringComponentInjectorRegistrar.java new file mode 100644 index 0000000000..b6fbc52051 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/SpringComponentInjectorRegistrar.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.spring.injection.annot.SpringComponentInjector; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Registers a {@link SpringComponentInjector} on every {@link WebApplication} bean as soon as the + * bean is created. + *

+ * Spring Boot initialises the embedded servlet container's filters, and with them the Wicket + * application, while the application context is still refreshing and before ordinary singletons + * exist. Registering the injector at bean creation is what makes {@code @SpringBean} (and + * {@link org.apache.wicket.injection.Injector#get()}) available inside + * {@link WebApplication#init()}, whichever filter registration ends up initialising the + * application. + *

+ * + * @author Daniel Bartl + */ +class SpringComponentInjectorRegistrar implements BeanPostProcessor, ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof WebApplication webApplication) { + webApplication.getComponentInstantiationListeners() + .add(new SpringComponentInjector(webApplication, applicationContext)); + } + return bean; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketAutoConfiguration.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketAutoConfiguration.java new file mode 100644 index 0000000000..e10b79537a --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketAutoConfiguration.java @@ -0,0 +1,130 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.WicketFilter; +import org.apache.wicket.spring.injection.annot.SpringComponentInjector; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * Spring Boot autoconfiguration for Apache Wicket. + *

+ * This autoconfiguration is activated when Wicket's core classes are present on the classpath. + * It registers Wicket's {@link WicketFilter} into the embedded servlet container and binds the + * registered {@link WebApplication} bean. Additionally, it registers the {@link SpringComponentInjector} + * to enable Spring bean injection (via {@link org.apache.wicket.spring.injection.annot.SpringBean}) + * inside Wicket pages and components. + *

+ *

+ * Set {@code wicket.enabled=false} to switch the whole autoconfiguration off. + *

+ * + * @author Daniel Bartl + */ +@AutoConfiguration +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass({WebApplication.class, WicketFilter.class}) +@ConditionalOnProperty(prefix = "wicket", name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(WicketProperties.class) +public class WicketAutoConfiguration { + + /** + * Registers a default {@link WebApplication} bean if the application does not define one. + * This registers the {@link DefaultWebApplication} which serves the starter's {@link DefaultHomePage}. + * + * @return the default WebApplication + */ + @Bean + @ConditionalOnMissingBean(WebApplication.class) + public WebApplication webApplication() { + return new DefaultWebApplication(); + } + + /** + * Registers a {@link SpringComponentInjector} on the {@link WebApplication} bean so that Wicket + * pages and components can inject Spring beans via + * {@link org.apache.wicket.spring.injection.annot.SpringBean}. + *

+ * The injector is added when the {@link WebApplication} bean is created, not later as a bean of + * its own: the servlet container initialises the Wicket filter, and so runs + * {@link WebApplication#init()}, before ordinary singletons exist. Being bound to the + * {@link WebApplication} rather than to the filter registration also keeps injection working + * for an application that supplies its own {@link FilterRegistrationBean}. + *

+ *

+ * Backs off when the application defines its own {@link SpringComponentInjector} bean. + *

+ * + * @return the post-processor registering the injector on each {@link WebApplication} bean + */ + @Bean + @ConditionalOnMissingBean(SpringComponentInjector.class) + static SpringComponentInjectorRegistrar springComponentInjectorRegistrar() { + return new SpringComponentInjectorRegistrar(); + } + + /** + * Creates and configures the {@link FilterRegistrationBean} for the {@link WicketFilter}. + *

+ * Backs off when the application already registers a {@link WicketFilter}, either wrapped in its + * own {@link FilterRegistrationBean} or declared as a plain filter bean, so that Wicket is never + * mapped twice. + *

+ * + * @param webApplication the auto-discovered Wicket WebApplication subclass bean + * @param properties the externalized Wicket properties + * @return the configured FilterRegistrationBean for WicketFilter + */ + @Bean + @ConditionalOnMissingFilterBean(WicketFilter.class) + public FilterRegistrationBean wicketFilterRegistration( + WebApplication webApplication, + WicketProperties properties) { + + return configureWicketFilter(properties, new WicketFilter(webApplication)); + } + + /** + * Configures a {@link FilterRegistrationBean} for a given {@link WicketFilter}. + * Sets up the filter mapping path, filter name, filter order and configuration type for Wicket. + * + * @param properties the {@link WicketProperties} containing configuration details such as filter path, + * filter name, filter order and runtime configuration type + * @param filter the {@link WicketFilter} to be configured and registered + * @return a configured {@link FilterRegistrationBean} instance for the provided {@link WicketFilter} + */ + private static FilterRegistrationBean configureWicketFilter( + WicketProperties properties, WicketFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(filter); + registration.addUrlPatterns(properties.getFilterPath()); + registration.setName(properties.getFilterName()); + registration.setOrder(properties.getFilterOrder()); + + // Specify configuration parameter mapping for Wicket + registration.addInitParameter(WicketFilter.FILTER_MAPPING_PARAM, properties.getFilterPath()); + registration.addInitParameter("configuration", properties.getConfiguration().name()); + return registration; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketProperties.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketProperties.java new file mode 100644 index 0000000000..cedd918e82 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/java/org/wicketstuff/springboot/starter/WicketProperties.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.RuntimeConfigurationType; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.Ordered; + +/** + * Configuration properties for Apache Wicket integration in Spring Boot. + *

+ * These properties can be customized in application configuration files (e.g., {@code application.properties} + * or {@code application.yml}) under the {@code wicket} prefix. + *

+ * + * @author Daniel Bartl + */ +@ConfigurationProperties(prefix = "wicket") +public class WicketProperties { + + /** + * URL mapping pattern for the Wicket Filter. + *

+ * Determines which incoming requests are processed by Wicket. Default is {@code "/*"}. + *

+ */ + private String filterPath = "/*"; + + /** + * Servlet name of the registered Wicket Filter. + *

+ * Helps identify the filter in registration logs and configuration contexts. Default is {@code "wicket-filter"}. + *

+ */ + private String filterName = "wicket-filter"; + + /** + * Wicket execution configuration type. + *

+ * Can be set to {@link RuntimeConfigurationType#DEVELOPMENT} or {@link RuntimeConfigurationType#DEPLOYMENT}. + * Default is {@link RuntimeConfigurationType#DEVELOPMENT}. + *

+ */ + private RuntimeConfigurationType configuration = RuntimeConfigurationType.DEVELOPMENT; + + /** + * Order of the Wicket Filter within the servlet filter chain. + *

+ * Lower values run earlier. Default is {@link Ordered#LOWEST_PRECEDENCE}, so Wicket runs after + * every other filter, e.g. after Spring Security's filter chain. + *

+ */ + private int filterOrder = Ordered.LOWEST_PRECEDENCE; + + /** + * Gets the URL mapping pattern for the Wicket Filter. + * + * @return the filter URL mapping path pattern + */ + public String getFilterPath() { + return filterPath; + } + + /** + * Sets the URL mapping pattern for the Wicket Filter. + * + * @param filterPath the filter URL mapping path pattern to set + */ + public void setFilterPath(String filterPath) { + this.filterPath = filterPath; + } + + /** + * Gets the servlet name of the registered Wicket Filter. + * + * @return the servlet filter name + */ + public String getFilterName() { + return filterName; + } + + /** + * Sets the servlet name of the registered Wicket Filter. + * + * @param filterName the servlet filter name to set + */ + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + /** + * Gets the Wicket execution configuration type. + * + * @return the configuration type + */ + public RuntimeConfigurationType getConfiguration() { + return configuration; + } + + /** + * Sets the Wicket execution configuration type. + * + * @param configuration the configuration type to set (DEVELOPMENT or DEPLOYMENT) + */ + public void setConfiguration(RuntimeConfigurationType configuration) { + this.configuration = configuration; + } + + /** + * Gets the order of the Wicket Filter within the servlet filter chain. + * + * @return the filter order + */ + public int getFilterOrder() { + return filterOrder; + } + + /** + * Sets the order of the Wicket Filter within the servlet filter chain. + * + * @param filterOrder the filter order to set; lower values run earlier + */ + public void setFilterOrder(int filterOrder) { + this.filterOrder = filterOrder; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..e53d54f497 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,14 @@ +{ + "properties": [ + { + "name": "wicket.enabled", + "type": "java.lang.Boolean", + "description": "Whether the Wicket auto-configuration is enabled. When false, no WebApplication, SpringComponentInjector or Wicket filter registration is contributed.", + "defaultValue": true + }, + { + "name": "wicket.filter-order", + "defaultValue": 2147483647 + } + ] +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring-devtools.properties b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000000..c9c8e021ac --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1,13 @@ +# Loads Wicket and wicketstuff jars (including this starter) in Spring Boot DevTools' restart +# classloader instead of its base classloader. +# +# Wicket deserializes pages from its page store with an ObjectInputStream that resolves classes +# through the nearest application classloader on the call stack. With Wicket in the base +# classloader, that resolves the application's page classes from the base classloader too, while +# @SpringBean proxies are rebuilt for the restart classloader's classes: restoring a stored page +# (the back button, or any page after a restart) then fails with a ClassCastException. +# +# Every jar that references Wicket types has to move together, or the two classloaders mismatch +# the other way round. +restart.include.wicket=/wicket-[\\w\\d-\\.]+\\.jar +restart.include.wicketstuff=/wicketstuff-[\\w\\d-\\.]+\\.jar diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..564dc1ac5d --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.wicketstuff.springboot.starter.WicketAutoConfiguration diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/DefaultHomePage.html b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/DefaultHomePage.html new file mode 100644 index 0000000000..5f1f88064b --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/DefaultHomePage.html @@ -0,0 +1,56 @@ + + + + + + Wicket Spring Boot Starter + + + +
+ +
+
+

It works!

+

+ This is the default page served by the Wicket Spring Boot Starter, running on + Apache Wicket 10.x. + It's shown because your application hasn't registered its own WebApplication + bean yet. +

+

Next steps

+

+ Replace this page by registering your own Wicket application as a Spring bean: +

+
    +
  1. Create a WebApplication subclass annotated with @Component.
  2. +
  3. Point its getHomePage() to your own WebPage.
  4. +
  5. Optionally inject Spring beans into your pages with @SpringBean.
  6. +
+

+ See the starter's + README + for the full Quickstart, or the + Apache Wicket documentation + to learn more about building pages and components. +

+
+
+
+ + diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/logo.png b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/logo.png new file mode 100644 index 0000000000..39ec54854b Binary files /dev/null and b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/logo.png differ diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/style.css b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/style.css new file mode 100644 index 0000000000..b6f4f908f2 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/main/resources/org/wicketstuff/springboot/starter/style.css @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +body, p, li, a { font-family: georgia, times, serif;font-size:13pt;} +h1, h2, h3 { font-family: "Helvetica Neue", helvetica, arial, sans-serif; } +body { margin:0;padding:0;} +#hd { + width : 100%; + height : 87px; + background-color : #092E67; + margin-top : 0; + padding-top : 10px; + border-bottom : 1px solid #888; + z-index : 0; +} +#ft { + position : absolute; + bottom : 0; + width : 100%; + height : 99px; + background-color : #6493D2; + border-top : 1px solid #888; + z-index : 0; +} +#logo,#bd { + width : 650px; + margin: 0 auto; + padding: 25px 50px 0 50px; +} +#logo h1 { + color : white; + font-size:36pt; + display: inline; +} +#logo img { + display:inline; + vertical-align: bottom; + margin-left : 50px; + margin-right : 5px; +} +body { margin-top : 0; padding-top : 0;} +#logo, #logo h1 { margin-top : 0; padding-top : 0;} +#bd { + position : absolute; + top : 75px; + bottom : 75px; + left : 50%; + margin-left : -325px; + z-index : 1; + overflow: auto; + background-color : #fff; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 0px 0px 10px #888; + -webkit-box-shadow: 0px 0px 10px #888; + box-shadow: 0px 0px 10px #888; +} +a, a:visited, a:hover, a:active { + color : #6493D2; +} +h2 { + padding : 0; margin:0; + font-size:36pt; + color:#FF5500; +} +h3 { + padding : 0; margin:0; + font-size:24pt; + color:#092E67; +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/BuildAlignedWithSpringBootTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/BuildAlignedWithSpringBootTest.java new file mode 100644 index 0000000000..36a7e5bb00 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/BuildAlignedWithSpringBootTest.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Guards the version overrides in {@code wicketstuff-spring-boot-starter-parent}. + *

+ * wicketstuff-core pins several libraries that Spring Boot also manages, and those inherited pins + * win over the imported {@code spring-boot-dependencies} BOM. The parent pom therefore overrides + * them with the versions Spring Boot ships, so the starter is built and tested against what its + * users actually run. This test fails as soon as an override drifts from the BOM, e.g. after + * Spring Boot is bumped or a dependency update touches one of the overrides. + *

+ *

+ * The overrides reach this test as {@code aligned.} system properties set by + * Surefire, keyed by the name of the matching property in the Spring Boot BOM. + *

+ * + * @author Daniel Bartl + */ +class BuildAlignedWithSpringBootTest { + + private static final String PREFIX = "aligned."; + + @Test + void versionOverridesMatchTheSpringBootBom() throws Exception { + String bom = Files.readString(Path.of(System.getProperty("springBootBom"))); + + Map expected = new TreeMap<>(); + Map actual = new TreeMap<>(); + System.getProperties().stringPropertyNames().stream() + .filter(name -> name.startsWith(PREFIX)) + .forEach(name -> { + String bomProperty = name.substring(PREFIX.length()); + expected.put(bomProperty, bomProperty(bom, bomProperty)); + actual.put(bomProperty, System.getProperty(name)); + }); + + assertThat(actual) + .as("version overrides in wicketstuff-spring-boot-starter-parent/pom.xml, keyed by the " + + "spring-boot-dependencies property they must equal") + .isNotEmpty() + .isEqualTo(expected); + } + + private static String bomProperty(String bom, String name) { + Matcher matcher = Pattern.compile("<" + Pattern.quote(name) + ">([^<]+)"; + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DefaultWebApplicationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DefaultWebApplicationTest.java new file mode 100644 index 0000000000..904b2e4fc7 --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DefaultWebApplicationTest.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.request.resource.PackageResourceReference; +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Daniel Bartl + */ +class DefaultWebApplicationTest { + + private WicketTester tester; + + @BeforeEach + void setUp() { + tester = new WicketTester(new DefaultWebApplication()); + } + + @AfterEach + void tearDown() { + tester.destroy(); + } + + @Test + void homePageIsTheDefaultHomePage() { + assertThat(tester.getApplication().getHomePage()).isEqualTo(DefaultHomePage.class); + } + + @Test + void rendersDefaultHomePageWithTheWicketVersionLabel() { + tester.startPage(DefaultHomePage.class); + + tester.assertRenderedPage(DefaultHomePage.class); + tester.assertComponent("version", Label.class); + } + + @Test + void linksItsStylesheetAndLogoAsPackageResourcesRatherThanFromTheApplicationRoot() { + tester.startPage(DefaultHomePage.class); + + String markup = tester.getLastResponseAsString(); + assertThat(markup) + .contains("wicket/resource/org.wicketstuff.springboot.starter.DefaultHomePage/style") + .contains("wicket/resource/org.wicketstuff.springboot.starter.DefaultHomePage/logo") + .doesNotContain("href=\"style.css\"") + .doesNotContain("src=\"logo.png\""); + } + + @Test + void servesTheStylesheetAsAPackageResource() { + tester.startResourceReference(new PackageResourceReference(DefaultHomePage.class, "style.css")); + + assertThat(tester.getLastResponse().getStatus()).isEqualTo(200); + } + + @Test + void servesTheLogoAsAPackageResource() { + tester.startResourceReference(new PackageResourceReference(DefaultHomePage.class, "logo.png")); + + assertThat(tester.getLastResponse().getStatus()).isEqualTo(200); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DevToolsRestartIncludeTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DevToolsRestartIncludeTest.java new file mode 100644 index 0000000000..d4f4b8be8c --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/DevToolsRestartIncludeTest.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Guards {@code META-INF/spring-devtools.properties}, which moves Wicket into Spring Boot + * DevTools' restart classloader. DevTools matches each {@code restart.include.*} pattern against + * the URL of every classpath entry; a jar that references Wicket types but stays in the base + * classloader breaks page deserialization, so every Wicket and wicketstuff jar must match and + * nothing else should. + * + * @author Daniel Bartl + */ +class DevToolsRestartIncludeTest { + + @Test + void restartIncludePatternsMatchExactlyTheWicketJarsOnTheClasspath() throws Exception { + List patterns = restartIncludePatterns(); + List jars = Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) + .map(Path::of) + .filter(path -> path.getFileName().toString().endsWith(".jar")) + .toList(); + + List wicketJars = jars.stream() + .filter(jar -> jar.toString().contains("/org/apache/wicket/")) + .map(DevToolsRestartIncludeTest::url) + .toList(); + List otherJars = jars.stream() + .filter(jar -> !jar.toString().contains("/org/apache/wicket/") + && !jar.toString().contains("/org/wicketstuff/")) + .map(DevToolsRestartIncludeTest::url) + .toList(); + + assertThat(wicketJars).as("Wicket jars on the test classpath").isNotEmpty() + .allSatisfy(url -> assertThat(matchesAny(patterns, url)).as(url).isTrue()); + assertThat(otherJars) + .allSatisfy(url -> assertThat(matchesAny(patterns, url)).as(url).isFalse()); + assertThat(matchesAny(patterns, url(Path.of("/repo/org/wicketstuff/wicketstuff-spring-boot-starter/" + + "10.12.0/wicketstuff-spring-boot-starter-10.12.0.jar")))) + .as("the starter itself").isTrue(); + } + + private static List restartIncludePatterns() throws Exception { + Properties properties = new Properties(); + try (InputStream in = DevToolsRestartIncludeTest.class + .getResourceAsStream("/META-INF/spring-devtools.properties")) { + assertThat(in).as("META-INF/spring-devtools.properties").isNotNull(); + properties.load(in); + } + return properties.stringPropertyNames().stream() + .filter(name -> name.startsWith("restart.include.")) + .map(name -> Pattern.compile(properties.getProperty(name))) + .toList(); + } + + private static boolean matchesAny(List patterns, String url) { + return patterns.stream().anyMatch(pattern -> pattern.matcher(url).find()); + } + + private static String url(Path jar) { + return jar.toUri().toString(); + } +} diff --git a/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/WicketAutoConfigurationTest.java b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/WicketAutoConfigurationTest.java new file mode 100644 index 0000000000..002539548c --- /dev/null +++ b/wicketstuff-spring-boot-starter-parent/wicketstuff-spring-boot-starter/src/test/java/org/wicketstuff/springboot/starter/WicketAutoConfigurationTest.java @@ -0,0 +1,218 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wicketstuff.springboot.starter; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.Filter; +import org.apache.wicket.Page; +import org.apache.wicket.application.IComponentInstantiationListener; +import org.apache.wicket.RuntimeConfigurationType; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.WicketFilter; +import org.apache.wicket.spring.injection.annot.SpringComponentInjector; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Daniel Bartl + */ +class WicketAutoConfigurationTest { + + private final WebApplicationContextRunner servletContextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WicketAutoConfiguration.class)); + + @Test + void registersDefaultWebApplicationAndFilterWhenNoneProvided() { + servletContextRunner.run(context -> { + assertThat(context).hasSingleBean(WebApplication.class); + assertThat(context.getBean(WebApplication.class)).isInstanceOf(DefaultWebApplication.class); + + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration.getFilter()).isInstanceOf(WicketFilter.class); + assertThat(registration.getUrlPatterns()).containsExactly("/*"); + assertThat(registration.getFilterName()).isEqualTo("wicket-filter"); + assertThat(registration.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + assertThat(registration.getInitParameters()) + .containsEntry("configuration", RuntimeConfigurationType.DEVELOPMENT.name()); + }); + } + + @Test + void bindsCustomWicketPropertiesOntoTheFilterRegistration() { + servletContextRunner + .withPropertyValues( + "wicket.filter-path=/app/*", + "wicket.filter-name=custom-wicket-filter", + "wicket.filter-order=-50", + "wicket.configuration=DEPLOYMENT") + .run(context -> { + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration.getUrlPatterns()).containsExactly("/app/*"); + assertThat(registration.getFilterName()).isEqualTo("custom-wicket-filter"); + assertThat(registration.getOrder()).isEqualTo(-50); + assertThat(registration.getInitParameters()) + .containsEntry("configuration", RuntimeConfigurationType.DEPLOYMENT.name()); + }); + } + + @Test + void backsOffWhenACustomWebApplicationBeanIsPresent() { + servletContextRunner.withUserConfiguration(CustomWebApplicationConfig.class).run(context -> { + assertThat(context).hasSingleBean(WebApplication.class); + assertThat(context.getBean(WebApplication.class)).isInstanceOf(CustomWebApplication.class); + }); + } + + @Test + void backsOffWhenACustomWicketFilterRegistrationIsPresent() { + servletContextRunner.withUserConfiguration(CustomFilterRegistrationConfig.class).run(context -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration.getFilterName()).isEqualTo("custom-wicket-filter"); + assertThat(registration.getUrlPatterns()).containsExactly("/custom/*"); + }); + } + + @Test + void backsOffWhenAPlainWicketFilterBeanIsPresent() { + servletContextRunner.withUserConfiguration(PlainWicketFilterConfig.class).run(context -> { + assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); + assertThat(context).hasSingleBean(WicketFilter.class); + }); + } + + @Test + void keepsRegisteringWicketNextToUnrelatedFilterRegistrations() { + servletContextRunner.withUserConfiguration(UnrelatedFilterRegistrationConfig.class).run(context -> + assertThat(context.getBeansOfType(FilterRegistrationBean.class)) + .containsOnlyKeys("unrelatedFilterRegistration", "wicketFilterRegistration")); + } + + @Test + void registersSpringInjectionOnTheWebApplicationAsSoonAsItIsCreated() { + servletContextRunner.run(context -> + assertThat(springComponentInjectors(context.getBean(WebApplication.class))).hasSize(1)); + } + + @Test + void registersSpringInjectionOnACustomWebApplication() { + servletContextRunner.withUserConfiguration(CustomWebApplicationConfig.class).run(context -> + assertThat(springComponentInjectors(context.getBean(WebApplication.class))).hasSize(1)); + } + + @Test + void leavesSpringInjectionAloneWhenACustomSpringComponentInjectorBeanIsPresent() { + servletContextRunner.withUserConfiguration(CustomSpringComponentInjectorConfig.class).run(context -> { + assertThat(context).doesNotHaveBean(SpringComponentInjectorRegistrar.class); + assertThat(springComponentInjectors(context.getBean(WebApplication.class))).isEmpty(); + }); + } + + @Test + void doesNotActivateWhenDisabledByProperty() { + servletContextRunner.withPropertyValues("wicket.enabled=false").run(context -> { + assertThat(context).doesNotHaveBean(WebApplication.class); + assertThat(context).doesNotHaveBean(FilterRegistrationBean.class); + assertThat(context).doesNotHaveBean(SpringComponentInjectorRegistrar.class); + }); + } + + @Test + void doesNotActivateOutsideServletWebApplications() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WicketAutoConfiguration.class)) + .run(context -> assertThat(context).doesNotHaveBean(WebApplication.class)); + } + + private static List springComponentInjectors(WebApplication application) { + List injectors = new ArrayList<>(); + application.getComponentInstantiationListeners().forEach(listener -> { + if (listener instanceof SpringComponentInjector) { + injectors.add(listener); + } + }); + return injectors; + } + + @Configuration + static class CustomWebApplicationConfig { + + @Bean + WebApplication webApplication() { + return new CustomWebApplication(); + } + } + + @Configuration + static class CustomFilterRegistrationConfig { + + @Bean + FilterRegistrationBean customWicketFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new WicketFilter(new CustomWebApplication())); + registration.addUrlPatterns("/custom/*"); + registration.setName("custom-wicket-filter"); + return registration; + } + } + + @Configuration + static class CustomSpringComponentInjectorConfig { + + @Bean + SpringComponentInjector springComponentInjector(WebApplication webApplication, ApplicationContext context) { + // Deliberately not added as a listener: the application takes over wiring injection itself + return new SpringComponentInjector(webApplication, context); + } + } + + @Configuration + static class PlainWicketFilterConfig { + + @Bean + WicketFilter wicketFilter() { + return new WicketFilter(new CustomWebApplication()); + } + } + + @Configuration + static class UnrelatedFilterRegistrationConfig { + + @Bean + FilterRegistrationBean unrelatedFilterRegistration() { + return new FilterRegistrationBean<>((request, response, chain) -> chain.doFilter(request, response)); + } + } + + static class CustomWebApplication extends WebApplication { + + @Override + public Class getHomePage() { + return DefaultHomePage.class; + } + } +}