diff --git a/.appveyor.yml b/.appveyor.yml index cb8e180e9..a20016cad 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,20 +2,12 @@ version: '{build}' environment: matrix: - - job_name: Java 8 - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 - PYTHON: "C:\\Python34-x64" - - job_name: Java 11 - JAVA_HOME: C:\Program Files\Java\jdk11 - PYTHON: "C:\\Python34-x64" - job_name: Java 17 appveyor_build_worker_image: Visual Studio 2019 JAVA_HOME: C:\Program Files\Java\jdk17 - PYTHON: "C:\\Python36-x64" - -install: - - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% - - pip install codecov --user + - job_name: Java 21 + appveyor_build_worker_image: Visual Studio 2022 + JAVA_HOME: C:\Program Files\Java\jdk21 build_script: - ./gradlew assemble --no-daemon @@ -24,7 +16,10 @@ test_script: on_success: - ./gradlew jacocoTestReport --no-daemon - - python -m codecov -f build\reports\jacoco\test\jacocoTestReport.xml -F windows + - ps: | + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe + .\codecov.exe -f build\reports\jacoco\test\jacocoTestReport.xml -F windows cache: - C:\Users\appveyor\.gradle\caches diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 19fd40f8c..540ee8042 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ patreon: guicey +custom: https://boosty.to/xvik diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f6d9be634..89ee9d124 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,49 +1,12 @@ version: 2 updates: -- package-ecosystem: gradle - directory: "/" - schedule: - interval: daily - time: "23:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: org.codehaus.groovy:groovy - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-json - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-macro - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-nio - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-sql - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-templates - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-test - versions: - - ">= 3.a, < 4" - - dependency-name: org.codehaus.groovy:groovy-xml - versions: - - ">= 3.a, < 4" - - dependency-name: org.glassfish.hk2:guice-bridge - versions: - - "> 2.6.1" - - dependency-name: ru.vyarus.mkdocs - versions: - - 2.1.0 - - 2.1.1 - - dependency-name: org.ow2.asm:asm - versions: - - "9.1" - - dependency-name: ru.vyarus.quality - versions: - - 4.5.0 - - dependency-name: org.junit.platform:junit-platform-testkit - versions: - - 1.7.1 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + - package-ecosystem: gradle + directory: "/" + schedule: + interval: daily + time: "23:00" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/pom.xml b/.github/pom.xml deleted file mode 100644 index e5487af6c..000000000 --- a/.github/pom.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - 4.0.0 - ru.vyarus - dropwizard-guicey - 5.6.0 - - - com.google.inject - guice - 5.1.0 - compile - - - com.google.inject.extensions - guice-servlet - 5.1.0 - compile - - - io.dropwizard - dropwizard-core - 2.1.0 - compile - - - ru.vyarus - generics-resolver - 3.0.3 - compile - - - org.junit.jupiter - junit-jupiter-api - 5.8.2 - provided - - - io.dropwizard - dropwizard-testing - 2.1.0 - provided - - - com.github.spotbugs - spotbugs-annotations - 4.7.0 - provided - - - org.glassfish.hk2 - guice-bridge - 2.6.1 - provided - - - - - - io.dropwizard - dropwizard-dependencies - 2.1.0 - import - pom - - - com.google.inject - guice-bom - 5.1.0 - import - pom - - - ru.vyarus - spock-junit5 - 1.0.0 - - - com.google.inject - guice - 5.1.0 - - - com.google.guava - guava - - - - - ru.vyarus - dropwizard-guicey - ${project.version} - - - com.google.guava - guava - 31.1-jre - - - org.glassfish.hk2 - guice-bridge - 2.6.1 - - - com.google.inject - guice - - - org.glassfish.hk2 - hk2-api - - - - - - - 5.1.0 - 2.1.0 - 2.6.1 - - - - xvik - Vyacheslav Rusakov - vyarus@gmail.com - - - dropwizard-guicey - Dropwizard guice integration - https://github.com/xvik/dropwizard-guicey - - https://github.com/xvik/dropwizard-guicey - scm:git:git://github.com/xvik/dropwizard-guicey - scm:git:git://github.com/xvik/dropwizard-guicey - - - - The MIT License - https://raw.githubusercontent.com/xvik/dropwizard-guicey/master/LICENSE - repo - - - - GitHub - https://github.com/xvik/dropwizard-guicey/issues - - diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 107c3414e..000000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - name: Java ${{ matrix.java }} - strategy: - # don't cancel remaining matrix steps on failure - fail-fast: false - matrix: - java: [8, 11, 17] - - steps: - - uses: actions/checkout@v2 - - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Build - run: | - chmod +x gradlew - ./gradlew assemble --no-daemon - - - name: Test - env: - GH_ACTIONS: true - run: ./gradlew check --no-daemon - - - name: Build coverage report - run: ./gradlew jacocoTestReport --no-daemon - - - uses: codecov/codecov-action@v2 - with: - files: build/reports/jacoco/test/jacocoTestReport.xml - flags: LINUX - fail_ci_if_error: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41462a8c0..ea78f706d 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,6 @@ $RECYCLE.BIN/ # Used by previous versions of JEnv .jenv-version + +# maven wrapper +target diff --git a/CHANGELOG.md b/CHANGELOG.md index 63dee8f5a..cdafed891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,274 @@ +### 8.0.0 (2025-10-24) +* Update to dropwizard 5 (requires java 17) +* Use guice version without bundled asm ('classes' classifier) +* Support field injections in application (to use injected services in run method) +* Customizable DefaultTestClientFactory: it is now possible to use default implementation + with customizations (override `configure` method) + - Add ApacheTestClientFactory: useful to support PATCH methods on jdk > 16 + - Add `apacheClient` (shortcut) configuration into `@TestGuiceyApp` and `@TestDropwizardApp` + to simplify usage of ApacheTestClientFactory with annotations + - Add `apacheClient()` (shortcut) method into extension and generic builders +* Shared state: + - State objects, implementing AutoClosable, now would be closed on application shutdown + - Add SharedConfigurationState.lookupOrCreate method to simplify static state usage +* Add a utility to simplify building application urls: AppUrlBuilder + - Automatically resolve context server port and mapped paths (previously only available in + tests with ClientSupport class) + - Could build rest url directly from rest classes and methods + - Special api for building from direct rest method calls (method arguments + automatically mapped into url path and query params in this case) + - General utility for resource methods analysis: ResourceAnalyzer (could be used in + various api, based on resource (stub) call) +* Unify ClientSupport and StubRest client APIs + - New api is a wrapper above jersey client api to simplify test-specific configuration and validation + (jersey api is still available). The request builder unifies all possible configurations in one place. + - New common base client class TestClient + - ClientSupport is a TestClient, but also could provide 3 special clients: appClient(), adminClient(), restClient() + (restClient() is the same as rest stubs RestClient) + - New sub clients could be created by applying additional path segments: + client.subClient("/sub/path/) + - External api client could be created with support.externalClient("http://external.com/") + - New client rest api based on real method calls: restClient(RestClass.class).method(mock -> mock.restMethod(args)).invoke() + (target path and method type resolved from annotations, arguments used for request configuration) + - Helper api for testing multipart requests: restClient(..).multipartMethod(..) + (simplifies multipart method arguments creation) + - Defaults mechanism: it is possible to declare default headers, cookies, etc. + on the client to be applied for all requests (evolution of StubRest client ideas) + - Add PATCH method shortcuts + - Add builder-style response assertions like: client.do_request.assertHeader("Name", val) + This allows checking response headers, cookies, status code, etc. in a chained calls style without additional variables + - Add connector switching api to ClientSupport: apacheClient(), urlconnectorClient() + This allows using different connectors within one test + (note that apache connector is required for PATCH calls and urlconnector better handles multipart requests) + - Client logging now logs multipart requests (now PAYLOAD_ANY, before it was PAYLOAD_TEXT) + - (BREAKING) previous target(String... path) methods replaced with string format: target(String, Object...args) + - (BREAKING) deprecated targetMain(): replaced with targetApp() + - (BREAKING) StubRest default status declaration removed as not useful + (required status could be declared now with the new request builder) +* Add test web client field injection: + - @WebClient for ClientSupport, @WebClient(App), @WebClient(Admin), @WebClient(Rest) for specific clients + - @WebResourceClient for resource client direct mapping (works for integration and stub rest tests) +* Add guicey event ApplicationStartingEvent thrown just before managed and web services startup +* Fix stubs rest too early startup, causing problems with jersey registrations in application run method + +### 7.2.1 (2025-05-12) +* Fix NoClassDefFoundError on guicey startup due to junit classes leak into core (#428) + +### 7.2.0 (2025-05-11) +* Update to dropwizard 4.0.13 +* Un-deprecate HK2 support (removed deprecation annotations, but soft deprecation message remain in javadoc) +* Add methods to the main builder (and hooks) to simplify usage without guicey bundle: + - .whenConfigurationReady(...) - delayed configuration (same as GuiceyBunle.run): + simplify extensions or guice modules registration, requiring configuration + - .onGuiceyStartup() - executes after injector creation (under run phase). + Useful for manual dropwizard configurations + - .onApplicationStartup() - executes after complete application startup (including guicey lightweight test) + - .onApplicationShutdown() - executes after application shutdown + - .listenServer() - shortcut for jetty server startup listen + - .listenJetty() - shortcut for jetty lifecycle listening + - .listenJersey() - shortcut for jersey startup events and requests listening +* Diagnostic reports: + - Add application startup (and shutdown) time detalization report: .printStartupTime() + * Add hook alias for showing report on compiled application: -Dguicey.hooks=startup-time + - Add guice provision time report (time of guice beans creation): .printGuiceProvisionTime() + * Add hook alias for showing report on compiled application: -Dguicey.hooks=provision-time + * GuiceProvisionTimeHook could be used in tests to record beans creation at runtime + - Add the shared state usage report: .printSharedStateUsage() + - Improve guice bindings report (.printGuiceBindings()): + * Fixed scope accuracy for linked bindings + * Fixed bindings for private modules (missed exposed linked bindings) +* Guicey bundles: + - Add "throws Exception" for GuiceyBundle#initialize() to simplify usage + - Support extensions registration in GuiceyBundle run (.extensions() and .extensionsOptional()) + * ManualExtensionsValidatedEvent moved from configuration into run phase + * As before, classpath scan performed under configuration phase (but actual extensions registration moved to run phase) + - Transitive guicey bundles (.bundles(...)) initialize immediately after registration (unify behavior with dropwizard bundles and guice modules) + - Add onApplicationShutdown() and listenJersey() listener methods for GuiceyEnvironment (GuiceyBundle.run) +* Add "throws Exception" for GuiceyConfigurationHook#configure() to simplify usage +* Private guice modules support: + - Add private modules analysis: extensions searched in private module bindings too + (also important for avoiding duplicate binding registration after classpath scan) + - Add AnalyzePrivateGuiceModules option (enabled by default) to disable private modules + analysis (in case of problems) + - Disabled modules remove would also affect private modules now (but only first level) +* Classpath scan: + - Add extensions scan filters: GuiceBundle.builder().autoConfigFilter(cls -> !cls.isAnnotationPresent(Skip.class)) + Could be used either to skip some classes from scanning (without @InvisibleForScanner) annotation + or to accept only annotated classes (spring style) (#419) + * Added ClassFilters utility with common predicates: .autoConfigFilter(ignoreAnnotated(Skip.class)) + - Scan could detect package-private and protected extensions with a new option: + GuiceyOptions.ScanProtectedClasses (by default, false) (#404) +* Improve disable extensions predicate (bundle.disable(...)): + - Fix predicate applied for extension too early (without installer set) + - Add disable predicates: Disables.jerseyExtension, Disabled.webExtension and Disables.installedBy + - Predicates for exact type (module, bundle etc.) in Disables now raise item type to simplify further declarations +* Shared state: + - (breaking) Tie a state key to the stored object type to simplify usage (type-safe) and force + state objects usage instead of whatever values + - Fix null value supplier behavior (not allowed): .get(key, supplier) + - Add Options (read only accessor) object: state.getOptions() + - Add .whenReady() method for reactive state value access + * Add .whenSharedStateReady() for GuiceyBootstrap and GuiceyEnvironment + (not required for the main bundle as there is withSharedState() method where whenReady() could be used directly) + - Shared state usage report could be obtained at any time directly from the shared state + object (sharedState.getAccessReport()) +* Tests: + - Add the ability to disable managed objects lifecycle for lightweight guicey tests + (start/stop methods on managed objects not called; might be useful for tests with mocks): + * new GuiceyTestSupport().disableManagedLifecycle() + * @TestGuiceyApp(.., managedLifecycle = false) + * TestGuiceyAppExtension.forApp(..).disableManagedLifecycle() + * TestSupport.build(App.class).runCoreWithoutManaged(..) + - Add manual configuration object creation support for junit 5 extensions registered in field (@EnableSetup) + and TestEnvironmentSetup: .config(() -> {...}) + - Add missed configOverride(key, value) method for a single key-value pair + - Add configuration modifiers (`ConfigModifier`) - an alternative for configuration override mechanism: + ability to modify configuration instance before application startup. + Supported by all test extensions (junit5 annotations, setup object, generic builders, command runner) + - Add custom configuration block for junit5 extensions and TestEnvironmentSetup (to simplify lambda-based configurations): .with({...}) + - Junit ExtensionContext object could be injected as test method parameter + - Debug option: + * Track guicey test extensions time (would appear when debug enabled) + * Improve debug report: setup objects and hooks registration point are clear now (with direct code links) + - Add injectOnce option into test extensions to call injectMembers once per test instance + (useful when TestInstance.Lifecycle.PER_CLASS used) (discussion #394) + - Setup objects (TestEnvironmentSetup): + * Add "throws Exception" for TestEnvironmentSetup#setup() to simplify usage + * TestExtension builder improvements (TestEnvironmentSetup#setup(TestExtension)): + - Add getJunitContext() method to be able to configure test application with full context access (discussion #388) + - Add test lifecycle listeners: could be registered with listen() method or lambda-based on* methods + and provide notifications for guicey extension lifecycle (app start/stop, before/after test). + This is a simple alternative to writing junit extensions for additional integrations (db, testcontainers etc.). + - Add junit extension debug state method isDebug() so setup objects could + show debug output when debug option is enabled on guicey extension + - Add shortcut method isApplicationStartedForClass() to simplify beforeAll/beforeEach extension lifecycle detection + - Add annotated fields search api: findAnnotatedFields(..) to simplify writing annotation-driven extensions + * Add automatic setup objects (TestEnvironmentSetup) loading with service loader (simplify plugging-in extensions) + * Add base class for annotated fields extensions: AnnotatedTestFieldSetup + Handles fields validation and value injection lifecycle, including proper nested tests support + (all new test extensions based on it) + - New field-based test extensions: + * Add test stub fields: @StubBean(Service.class) ServiceStub + (use guice modules override feature to replace existing service into stub) + * Add mockito mock support: @MockBean Service. Mock automatically created + and override real dependency (module overrides used) + * Add mockito spy support: @SpyBean Service. Spy automatically created + and "proxy" real service (using aop) + * Add service trackers: @TrackBean Tracker. Tracker records all service + methods execution and could provide recordings for test verification or print performance + stats. A simpler replacement for mockito stubs. It could be used with mocks, spies and stubs + * Add REST stub (@StubRest): ability to start rest (or part of rest services) under @TestGuiceyApp + (without starting full container; same as dropwizard's ResourceExtension) + * Add logs testing support (@RecordLogs): record required logs for validation (only logback) + - Add option to disable default (new) annotated fields extensions: useDefaultExtensions +* Internal: + - Add BeforeInit guicey event (the first point with available Bootstrap) + - Add WebInstaller marker interface to identify web extensions (extensions started with jersey) + +NOTE on Gradle compatibility: +- Due to update to junit 5.12, there might be problems with platform-launcher dependency. The fix: + `testRuntimeOnly("org.junit.platform:junit-platform-launcher")` + (https://dev.to/be-hase/important-notes-on-junit-5120-in-gradle-13fj) + +### 7.1.4 (2024-09-14) +* Update to dropwizard 4.0.8 + +### 7.1.3 (2024-03-31) +* Update to dropwizard 4.0.7 +* Fix guicey ApplicationShutdownEvent typo (#387) + +NOTE: If your code uses this event directly (name with typo: ApplicationShotdownEvent), then it would +be a breaking change and event name must be corrected manually in your code (to ApplicationShutdownEvent). +Sorry, can't do it in a backwards-compatible way, but I assume rare usage so should not affect many. + +### 7.1.2 (2024-02-17) +* Update to dropwizard 4.0.6 + +### 7.1.1 (2024-01-08) +* Update to dropwizard 4.0.5 + +### 7.1.0 (2023-11-28) +* Update to dropwizard 4.0.4 +* Add qualifier annotations support for configuration properties binding: + any configuration property (any level), annotated with qualifier annotation, would be + directly bound with that qualifier. Core dropwizard objects could be qualified on overridden getter +* Test improvements: + - Junit 5 extensions could inject DropwizardTestSupport object itself as test method parameter + - ClientSupport: + * inner jersey client creation is customizable now with TestClientFactory implementation + (new attribute "clientFactory" in @TestGuiceyApp and @TestDropwizardApp) + * default factory would automatically configure: + - multipart feature if available in classpath (dropwizard-forms) + - direct console logging (to see requests and responses directly in console) + * New methods: + - basePathRoot - root url (only with port) + - get(), post(), delete(), put() - simple shortcut methods to perform basic operations relative to server root + - Context support object (DropwizardTestSupport) and client (ClientSupport) instances are accessible now statically + for both manual run (TestSupport) and junit extensions: TestSupport.getContext() and TestSupport.getContextClient() + - New generic builder for flexible DropwizardTestSupport object creation and run (when junit extension can't be used): + TestSupport.builder() (with lifecycle listeners support) + - TestSupport methods changes: + * Creation and run methods updated with config override (strings) support + * Add creation and run methods application class only (and optional overrides). + * Run methods without callback now return RunResult containing all objects, required for validation (for example, to examine config) + * Add captureOutput method to record console output for assertions + - Commands test support: + * TestSupport.buildCommandRunner() - builds runner for command execution + with the same builder options as in generic builder (TestSupport.builder(); including same configuration) + and user input support. + * Could be used to test application startup fail (without using system mocks) + +### 7.0.2 (2023-10-06) +* Update to dropwizard 4.0.2 + +### 7.0.1 (2023-07-05) +* Update to dropwizard 4.0.1 +* [jdbi] + - Fix jdbi 3.39 compatibility + - Avoid redundant transaction isolation level checks (extra queries) (#318) +* [gsp] + - Fix redirection to error page after direct template rendering fails + +### 7.0.0 (2023-05-14) +* Update to dropwizard 4 + - (breaking) Use jakarta namespace instead of javax (servlet, validation) +* Update to guice 7 (jakarta.inject namespace) + +### 6.1.0 (2023-05-14) +* Update to guice 6.0 + +### 6.0.0 (2023-04-02) +* Update to dropwizard 3 + - (breaking) Drop java 8 support +* Merged with guicey-ext modules repository: + - Ext modules version would be the same as guicey + - dropwizard-guicey POM would not be a BOM anymore (everything moved to guicey-bom) + - Exclusions not applied in BOM anymore, instead they applied directly in POM + +### 5.7.1 (2023-03-09) +* Update to dropwizard 2.1.5 +* Revert changing reports log level: now INFO used instead of WARN (#276) + +### 5.7.0 (2022-12-29) +* Update to dropwizard 2.1.4 +* Fix NoClassDefFoundError(AbstractCollectionJaxbProvider) appeared for some jersey provider registrations (#240) +* Jersey extensions might omit `@Provider` on known extension types (ExceptionMapper, MessageBodyReader, etc.). + Unifies usage with pure dropwizard (no additional `@Provider` annotation required). (#265) + - New option InstallerOptions.JerseyExtensionsRecognizedByType could disable new behaviour +* Support ModelProcessor jersey extension installation (#186) +* Add extensions help: .printExtensionsHelp() showing extension signs recognized by installers (in recognition order) + - Custom installers could participate in report by overriding FeatureInstaller.getRecognizableSigns() + (default interface method). +* Change reports log level from INFO to WARN to comply with default dropwizard level +* Support application reuse between tests (#269) + - new reuseApplication parameter in extensions enables reuse + - reusable application must be declared in base test class: all tests derived + from this base class would use the same application instance +* Add SBOM (json and xml with cyclonedx classifier) +* Add .enableAutoConfig() no-args shortcut for enabling classpath scan in application package + +### 5.6.1 (2022-07-02) +* Update dropwizard to 2.1.1 (fixes java 8 issue by allowing afterburner usage) * Fix classpath scan recognition of inner static classes inside jars (#231) * Junit 5 extensions: - Fix parallel test methods support (configuration overrides were applied incorrectly) diff --git a/LICENSE b/LICENSE index 2c7c39101..cb76e5fd2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014, Vyacheslav Rusakov +Copyright (c) 2014-2025, Vyacheslav Rusakov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md deleted file mode 100644 index 1d8da2818..000000000 --- a/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# Dropwizard guice integration -[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://www.opensource.org/licenses/MIT) -[![CI](https://github.com/xvik/dropwizard-guicey/actions/workflows/CI.yml/badge.svg)](https://github.com/xvik/dropwizard-guicey/actions/workflows/CI.yml) -[![Appveyor build status](https://ci.appveyor.com/api/projects/status/github/xvik/dropwizard-guicey?svg=true&branch=master)](https://ci.appveyor.com/project/xvik/dropwizard-guicey) -[![codecov](https://codecov.io/gh/xvik/dropwizard-guicey/branch/master/graph/badge.svg)](https://codecov.io/gh/xvik/dropwizard-guicey) - -**DOCUMENTATION**: http://xvik.github.io/dropwizard-guicey/ - -Additional repositories: - -* [Examples](https://github.com/xvik/dropwizard-guicey-examples) -* [Extensions and integrations](https://github.com/xvik/dropwizard-guicey-ext) - -Support: [discussions](https://github.com/xvik/dropwizard-guicey/discussions) | [gitter chat](https://gitter.im/xvik/dropwizard-guicey) - -### About - -[Dropwizard](http://dropwizard.io/) 2.1.0 [guice](https://github.com/google/guice) 5.1.0 integration. - -Features: - -* Auto configuration from classpath scan and guice bindings. -* Yaml config values bindings by path or unique sub objects. -* Advanced Web support -* Dropwizard style console reporting: detected (and installed) extensions are printed to console to remove uncertainty -* Test support: custom junit and spock extensions -* Advanced test abilities to disable or override application logic -* Developer friendly: - - core integrations may be replaced (to better fit needs) - - rich api for developing custom integrations, and hooking into lifecycle) - - out of the box support for plug-n-play plugins (auto discoverable) - - diagnostic tools (reports), support for custom diagnostic tools - -### Sponsors - -    [![Channel](src/doc/docs/img/sponsors/zoyi-ch.png)](https://channel.io "Channel") - - -If guicey makes your life easier, you can [support its development](https://www.patreon.com/guicey). - -#### Thanks to - -* [Sébastien Boulet](https://github.com/gontard) ([intactile design](http://intactile.com)) for very useful feedback -* [Nicholas Pace](https://github.com/segfly) for governator integration - -### Setup - -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus/dropwizard-guicey.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus/dropwizard-guicey) - -May be used through [extensions project BOM](https://github.com/xvik/dropwizard-guicey-ext) or directly. - -Maven: - -```xml - - ru.vyarus - dropwizard-guicey - 5.6.0 - -``` - -Gradle: - -```groovy -implementation 'ru.vyarus:dropwizard-guicey:5.6.0' -``` - -Dropwizard | Guicey -----------|--------- -2.1| [5.6.0](http://xvik.github.io/dropwizard-guicey/5.6.0) -2.0| [5.5.0](http://xvik.github.io/dropwizard-guicey/5.5.0) -1.3| [4.2.3](http://xvik.github.io/dropwizard-guicey/4.2.3) -1.1, 1.2 | [4.1.0](http://xvik.github.io/dropwizard-guicey/4.1.0) -1.0 | [4.0.1](http://xvik.github.io/dropwizard-guicey/4.0.1) -0.9 | [3.3.0](https://github.com/xvik/dropwizard-guicey/tree/dw-0.9) -0.8 | [3.1.0](https://github.com/xvik/dropwizard-guicey/tree/dw-0.8) -0.7 | [1.1.0](https://github.com/xvik/dropwizard-guicey/tree/dw-0.7) - - -#### BOM - -Guicey pom may be also used as maven BOM. - -NOTE: If you use guicey extensions then use [extensions BOM](https://github.com/xvik/dropwizard-guicey-ext) -instead (it already includes guicey BOM). - -BOM usage is highly recommended as it allows you to correctly update dropwizard dependencies. - -Gradle: - -```groovy -dependencies { - implementation platform('ru.vyarus:dropwizard-guicey:5.6.0') - // uncomment to override dropwizard and its dependencies versions - //implementation platform('io.dropwizard:dropwizard-dependencies:2.1.0') - - // no need to specify versions - implementation 'ru.vyarus:dropwizard-guicey' - - implementation 'io.dropwizard:dropwizard-auth' - implementation 'com.google.inject:guice-assistedinject' - - testImplementation 'ru.vyarus:spock-junit5' - testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0' - testImplementation 'io.dropwizard:dropwizard-test' -} -``` - -Maven: - -```xml - - - - ru.vyarus - dropwizard-guicey - 5.6.0 - pom - import - - - - - - - - ru.vyarus - dropwizard-guicey - - -``` - -BOM includes: - -BOM | Artifact ---------------|------------------------- -Guicey itself | `ru.vyarus:dropwizard-guicey` -Dropwizard BOM | `io.dropwizard:dropwizard-bom` -Guice BOM | `com.google.inject:guice-bom` -HK2 bridge | `org.glassfish.hk2:guice-bridge` -Spock-junit5 | `ru.vyarus:spock-junit5` - - -### Snapshots - -
- Snapshots may be used through JitPack - -Add [JitPack](https://jitpack.io/#ru.vyarus/dropwizard-guicey) repository: - -```groovy -repositories { maven { url 'https://jitpack.io' } } -``` - -For spring dependencies plugin (when guicey pom used as BOM): - -```groovy -dependencyManagement { - resolutionStrategy { - cacheChangingModulesFor 0, 'seconds' - } - imports { - mavenBom "ru.vyarus:dropwizard-guicey:master-SNAPSHOT" - } -} -``` - -For direct guicey dependency: - -```groovy -configurations.all { - resolutionStrategy.cacheChangingModulesFor 0, 'seconds' -} - -dependencies { - implementation 'ru.vyarus:dropwizard-guicey:master-SNAPSHOT' -} -``` - -Note that in both cases `resolutionStrategy` setting required for correct updating snapshot with recent commits -(without it you will not always have up-to-date snapshot) - -OR you can depend on exact commit: - -* Go to [JitPack project page](https://jitpack.io/#ru.vyarus/dropwizard-guicey) -* Select `Commits` section and click `Get it` on commit you want to use and - use commit hash as version: `ru.vyarus:dropwizard-guicey:56537f7d23` - - -Maven: - -```xml - - - jitpack.io - https://jitpack.io - - - - - - - ru.vyarus - dropwizard-guicey - master-SNAPSHOT - pom - import - - - - - - - ru.vyarus - dropwizard-guicey - - -``` - -Or simply change version if used as direct dependency (repository must be also added): - -```xml - - ru.vyarus - dropwizard-guicey - master-SNAPSHOT - -``` - -
- -### Usage - -Read [documentation](http://xvik.github.io/dropwizard-guicey/) - -### Might also like - -* [yaml-updater](https://github.com/xvik/yaml-updater) - yaml configuration update tool, preserving comments and whitespaces (has dropwizard module) -* [generics-resolver](https://github.com/xvik/generics-resolver) - runtime generics resolution -* [guice-validator](https://github.com/xvik/guice-validator) - hibernate validator integration for guice -(objects validation, method arguments and return type runtime validation) -* [guice-ext-annotations](https://github.com/xvik/guice-ext-annotations) - @Log, @PostConstruct, @PreDestroy and -utilities for adding new annotations support -* [guice-persist-orient](https://github.com/xvik/guice-persist-orient) - guice integration for orientdb -* [dropwizard-orient-server](https://github.com/xvik/dropwizard-orient-server) - embedded orientdb server for dropwizard - ---- -[![java lib generator](http://img.shields.io/badge/Powered%20by-%20Java%20lib%20generator-green.svg?style=flat-square)](https://github.com/xvik/generator-lib-java) diff --git a/build.gradle b/build.gradle index b116d3a97..9b3557b87 100644 --- a/build.gradle +++ b/build.gradle @@ -1,163 +1,226 @@ plugins { - id 'groovy' + id 'ru.vyarus.github-info' version '2.0.0' apply false + id 'ru.vyarus.quality' version '6.0.1' apply false + id 'com.github.spotbugs' version '6.4.7' apply false + id 'org.cyclonedx.bom' version '3.1.0' apply false + id 'ru.vyarus.mkdocs' version '4.0.1' apply false + id 'jacoco' - id 'project-report' - id 'signing' - id 'ru.vyarus.java-lib' version '2.3.0' - id 'ru.vyarus.github-info' version '1.3.0' - id 'ru.vyarus.quality' version '4.7.0' - id 'net.researchgate.release' version '3.0.0' - id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' - id 'com.github.ben-manes.versions' version '0.42.0' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' - id 'ru.vyarus.mkdocs' version '2.4.0' + id 'java-platform' + id 'ru.vyarus.java-lib' version '3.0.0' + id 'net.researchgate.release' version '3.1.0' + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' + id 'com.github.ben-manes.versions' version '0.53.0' } -sourceCompatibility = 1.8 - wrapper { - gradleVersion = '7.4' + gradleVersion = '8.10.1' } +description = 'Dropwizard guice integration' + ext { - dropwizard = '2.1.0' - guice = '5.1.0' - hk2 = '2.6.1' + dropwizard = '5.0.0' + guice = '7.0.0' + asm = '9.9' + hk2 = '3.0.6' + spockJunit5 = '1.2.0' + guiceExtAnn = '2.0.1' + guiceValidator = '3.0.2' } -repositories { mavenLocal(); mavenCentral() } -dependencyManagement { - imports { - mavenBom "com.google.inject:guice-bom:$guice" - mavenBom "io.dropwizard:dropwizard-dependencies:$dropwizard" +// root project is a BOM +dependencies { + // dropwizard BOM must go first to let maven select correct guava + api platform("io.dropwizard:dropwizard-dependencies:$dropwizard") + api platform("com.google.inject:guice-bom:$guice") + + constraints { + // use classes version without bundled asm to be able to easily update asm version + api "com.google.inject:guice:classes:$guice" + api "org.ow2.asm:asm:$asm" + api "org.glassfish.hk2:guice-bridge:$hk2" + api "ru.vyarus:spock-junit5:$spockJunit5" + + api "ru.vyarus:guice-ext-annotations:$guiceExtAnn" + api "ru.vyarus:guice-validator:$guiceValidator" + + // add subprojects to BOM + project.subprojects.each { api it } } - // exclusions here mostly fixes conflicts for maven projects - dependencies { - // force guava version from dropwizard bom - dependency "com.google.guava:guava:${dependencyManagement.importedProperties['guava.version']}" - dependency "org.glassfish.hk2:guice-bridge:$hk2", { - exclude 'com.google.inject:guice' - exclude 'org.glassfish.hk2:hk2-api' - } - // SPOCK excluded from bom because it would complicate older version usage in gradle - dependency 'ru.vyarus:spock-junit5:1.0.0' - - dependency "com.google.inject:guice:$guice", { exclude 'com.google.guava:guava' } +} - // add guicey itself to BOM (for version management) - dependency 'ru.vyarus:dropwizard-guicey:${project.version}' +javaLib { + // aggregated test and coverage reports + aggregateReports() + // publish root BOM as custom artifact + bom { + artifactId = 'guicey-bom' + description = 'Guicey BOM' } } -dependencies { - provided 'org.junit.jupiter:junit-jupiter-api' - provided 'io.dropwizard:dropwizard-testing' - provided 'com.github.spotbugs:spotbugs-annotations:4.7.1' - provided "org.glassfish.hk2:guice-bridge" - - implementation 'com.google.inject:guice' - implementation 'com.google.inject.extensions:guice-servlet' - implementation 'io.dropwizard:dropwizard-core' - implementation 'ru.vyarus:generics-resolver:3.0.3' - - testImplementation 'ru.vyarus:spock-junit5' - testImplementation 'org.spockframework:spock-core:2.2-M1-groovy-4.0' - - testImplementation 'org.glassfish.jersey.inject:jersey-hk2' - testImplementation 'io.dropwizard:dropwizard-auth' - testImplementation 'org.glassfish.jersey.ext:jersey-proxy-client' - testImplementation 'org.junit.platform:junit-platform-testkit' - testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.0.1' - testImplementation "com.google.truth:truth:1.1.3" - - // required for pure junit 5 tests - testRuntimeOnly 'org.junit.jupiter:junit-jupiter' + +maven.pom { + properties = [ + 'guice.version' : guice, + 'dropwizard.version' : dropwizard, + 'hk2.version' : hk2, + 'guice-ext-annotations.version': guiceExtAnn + ] } -group = 'ru.vyarus' -description = 'Dropwizard guice integration' +// maven publication related configuration applied to all projects +allprojects { + apply plugin: 'project-report' + apply plugin: 'ru.vyarus.github-info' + apply plugin: 'ru.vyarus.java-lib' + apply plugin: 'signing' -github { - user = 'xvik' - license = 'MIT' -} + repositories { mavenLocal(); mavenCentral(); maven { url 'https://jitpack.io' } } -mkdocs { - publish { - docPath = '5.6.0' - rootRedirect = true - rootRedirectTo = 'latest' - versionAliases = ['latest'] + configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } - extras = [ - 'version': '5.6.0', - 'ext': '5.6.0-1', - 'dropwizard': project.dropwizard, - 'guice': project.guice - ] -} -pom { - delegate.properties { - 'guice.version' guice - 'dropwizard.version' dropwizard - 'hk2.version' hk2 + group = 'ru.vyarus.guicey' + + github { + user = 'xvik' + license = 'MIT' + repository = 'dropwizard-guicey' } - developers { - developer { - id 'xvik' - name 'Vyacheslav Rusakov' - email 'vyarus@gmail.com' + + // delay required because java plugin is activated only in subprojects and without it + // pom closure would reference root project only + pluginManager.withPlugin("ru.vyarus.pom") { + maven.pom { + developers { + developer { + id = 'xvik' + name = 'Vyacheslav Rusakov' + email = 'vyarus@gmail.com' + } + } } } -} -javaLib { - // java 9 auto module name - autoModuleName = 'ru.vyarus.dropwizard.guicey' // don't publish gradle metadata artifact - withoutGradleMetadata() - // put resolved dependencies versions - pom.forceVersions() + javaLib.withoutGradleMetadata() + + // skip signing for jitpack (snapshots) + tasks.withType(Sign) { onlyIf { !System.getenv('JITPACK') && !version.endsWith('-SNAPSHOT') } } } +// all sub-modules are normal java modules, using root BOM (like maven) +subprojects { + apply plugin: 'groovy' + apply plugin: 'jacoco' + apply plugin: 'ru.vyarus.quality' + apply plugin: 'com.github.ben-manes.versions' + apply plugin: 'org.cyclonedx.bom' + + java { + sourceCompatibility = JavaVersion.VERSION_17 + } + + quality { + spotbugsQuiet = true + } + + dependencies { + implementation platform(project(':')) + } + + if (project.name != 'dropwizard-guicey') { + // extra modules use different group + group = 'ru.vyarus.guicey' + + // common dependencies for all modules except core + dependencies { + implementation project(':dropwizard-guicey') + + testImplementation 'io.dropwizard:dropwizard-testing' + } + + javaLib { + // java 9 auto module name + autoModuleName = "$group.${project.name.replace('guicey', 'ru.vyarus.dropwizard.guicey').replace('-', '.')}" + } + + // use only direct dependencies in the generated pom, removing BOM mentions + maven.removeDependencyManagement() + } + + test { + testLogging { + events 'skipped', 'failed' + exceptionFormat = 'full' + } + maxHeapSize = '512m' + } + + dependencyUpdates.revision = 'release' + + if (!project.name.startsWith('guicey-test-')) { + test { + useJUnitPlatform() + } + dependencies { + testImplementation 'ru.vyarus:spock-junit5' + testImplementation 'org.spockframework:spock-core:2.4-M5-groovy-4.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + + // required for junit 5.12 in current gradle version + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + } else { + // don't compile and run tests for legacy junit4 and spock1 modules + tasks.withType(GroovyCompile) + .configureEach { it.onlyIf { JavaVersion.current() <= JavaVersion.VERSION_11 } } + test.onlyIf { JavaVersion.current() <= JavaVersion.VERSION_11 } + } + + // SBOM + tasks.cyclonedxDirectBom { + includeConfigs = ["runtimeClasspath"] + } + publishing.publications.maven { + artifact(tasks.cyclonedxDirectBom.jsonOutput.get()) { + classifier = 'cyclonedx' + builtBy cyclonedxDirectBom + } + artifact(tasks.cyclonedxDirectBom.xmlOutput.get()) { + classifier = 'cyclonedx' + builtBy cyclonedxDirectBom + } + } +} + +// dependency on all subprojects required for release validation +check.dependsOn subprojects.check + nexusPublishing { repositories { sonatype { + nexusUrl = uri("https://ossrh-staging-api.central.sonatype.com/service/local/") + snapshotRepositoryUrl = uri("https://central.sonatype.com/repository/maven-snapshots/") username = findProperty('sonatypeUser') password = findProperty('sonatypePassword') } } } -// skip signing for jitpack (snapshots) -tasks.withType(Sign) {onlyIf { !System.getenv('JITPACK') }} - // Required signing properties for release: signing.keyId, signing.password and signing.secretKeyRingFile // (https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signatory_credentials) release.git.requireBranch.set('master') +// release manages only root project (subprojects will be checked and released implicitly) afterReleaseBuild { - dependsOn = ['publishToSonatype', - 'closeAndReleaseSonatypeStagingRepository'] + dependsOn 'publishToSonatype' + dependsOn subprojects.collect { ":$it.name:publishToSonatype" } + dependsOn 'closeAndReleaseSonatypeStagingRepository' doLast { logger.warn "RELEASED $project.group:$project.name:$project.version" } -} - -test { - useJUnitPlatform() - testLogging { - events 'skipped', 'failed' - exceptionFormat 'full' - } - maxHeapSize = '512m' -} - -dependencyUpdates.revision = 'release' - -task updateGithubPom(type: Copy, group: 'other') { - from(generatePomFileForMavenPublication) - into '.github' - rename 'pom-default.xml', 'pom.xml' } \ No newline at end of file diff --git a/dropwizard-guicey/README.md b/dropwizard-guicey/README.md new file mode 100644 index 000000000..a99860b75 --- /dev/null +++ b/dropwizard-guicey/README.md @@ -0,0 +1,20 @@ +# Dropwizard-guicey + +Core dropwizard-guicey library. + + +Maven: + +```xml + + ru.vyarus + dropwizard-guicey + { guicey.version } + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus:dropwizard-guicey:{ guicey.version }' +``` diff --git a/dropwizard-guicey/build.gradle b/dropwizard-guicey/build.gradle new file mode 100644 index 000000000..4aed1e531 --- /dev/null +++ b/dropwizard-guicey/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'java-library' + id 'ru.vyarus.mkdocs' +} + +dependencies { + provided 'org.junit.jupiter:junit-jupiter-api' + provided 'io.dropwizard:dropwizard-testing' + provided 'org.glassfish.jersey.media:jersey-media-multipart' + provided ('org.glassfish.hk2:guice-bridge') { + exclude group: 'com.google.inject', module: 'guice' + } + + api ('com.google.inject:guice') { + artifact { + classifier = 'classes' + } + // use dropwizard guava version + exclude group: 'com.google.guava', module: 'guava' + } + api 'org.ow2.asm:asm' + api ('com.google.inject.extensions:guice-servlet') { + exclude group: 'com.google.inject', module: 'guice' + } + api 'io.dropwizard:dropwizard-core' + api 'ru.vyarus:generics-resolver:3.0.3' + + optional 'org.mockito:mockito-core' + + testImplementation 'org.glassfish.jersey.inject:jersey-hk2' + testImplementation 'io.dropwizard:dropwizard-auth' + testImplementation 'io.dropwizard:dropwizard-forms' + testImplementation 'org.glassfish.jersey.ext:jersey-proxy-client' + testImplementation 'org.junit.platform:junit-platform-testkit' + testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8' + testImplementation 'com.google.truth:truth:1.4.5' + testImplementation 'org.objenesis:objenesis:3.4' + // test rest stubs + //testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2' + + // required for pure junit 5 tests + testRuntimeOnly 'org.junit.jupiter:junit-jupiter' +} + +// core module group is different! +group = 'ru.vyarus' +description = 'Dropwizard guice integration' + +javaLib { + // java 9 auto module name + autoModuleName = 'ru.vyarus.dropwizard.guicey' +} + +// use only direct dependencies in the generated pom, removing BOM mentions +maven.removeDependencyManagement() + +mkdocs { + publish { + docPath = '8.0.0' + rootRedirect = false + rootRedirectTo = 'latest' + versionAliases = ['latest'] + hideOldBugfixVersions = true + } + extras = [ + 'version': '8.0.0', + 'dropwizard': rootProject.dropwizard, + 'guice': rootProject.guice + ] +} \ No newline at end of file diff --git a/src/doc/docs/about/compatibility.md b/dropwizard-guicey/src/doc/docs/about/compatibility.md similarity index 69% rename from src/doc/docs/about/compatibility.md rename to dropwizard-guicey/src/doc/docs/about/compatibility.md index 0aa202d15..b00a38323 100644 --- a/src/doc/docs/about/compatibility.md +++ b/dropwizard-guicey/src/doc/docs/about/compatibility.md @@ -2,7 +2,10 @@ Dropwizard | Guicey ----------|--------- -2.1.0 | [5.6.0](http://xvik.github.io/dropwizard-guicey/5.6.0) +5.0.0 | [8.0.0](http://xvik.github.io/dropwizard-guicey/8.0.0) +4.0.0 | [7.3.0](http://xvik.github.io/dropwizard-guicey/7.3.0) +3.0.0 | [6.4.0](http://xvik.github.io/dropwizard-guicey/6.4.0) +2.1.0 | [5.10.2](http://xvik.github.io/dropwizard-guicey/5.10.2) 2.0.0 | [5.5.0](http://xvik.github.io/dropwizard-guicey/5.5.0) 1.3.0 | [4.2.3](http://xvik.github.io/dropwizard-guicey/4.2.3) 1.2.0 | [4.1.0](http://xvik.github.io/dropwizard-guicey/4.1.0) diff --git a/src/doc/docs/about/history.md b/dropwizard-guicey/src/doc/docs/about/history.md similarity index 67% rename from src/doc/docs/about/history.md rename to dropwizard-guicey/src/doc/docs/about/history.md index 9f5269e98..5c84f7778 100644 --- a/src/doc/docs/about/history.md +++ b/dropwizard-guicey/src/doc/docs/about/history.md @@ -1,9 +1,280 @@ +### [8.0.0](http://xvik.github.io/dropwizard-guicey/8.0.0) (2025-10-24) +* Update to dropwizard 5 (requires java 17) +* Use guice version without bundled asm ('classes' classifier) +* Support field injections in application (to use injected services in run method) +* Customizable DefaultTestClientFactory: it is now possible to use default implementation + with customizations (override `configure` method) + - Add ApacheTestClientFactory: useful to support PATCH methods on jdk > 16 + - Add `apacheClient` (shortcut) configuration into `@TestGuiceyApp` and `@TestDropwizardApp` + to simplify usage of ApacheTestClientFactory with annotations + - Add `apacheClient()` (shortcut) method into extension and generic builders +* Shared state: + - State objects, implementing AutoClosable, now would be closed on application shutdown + - Add SharedConfigurationState.lookupOrCreate method to simplify static state usage +* Add a utility to simplify building application urls: AppUrlBuilder + - Automatically resolve context server port and mapped paths (previously only available in + tests with ClientSupport class) + - Could build rest url directly from rest classes and methods + - Special api for building from direct rest method calls (method arguments + automatically mapped into url path and query params in this case) + - General utility for resource methods analysis: ResourceAnalyzer (could be used in + various api, based on resource (stub) call) +* Unify ClientSupport and StubRest client APIs + - New api is a wrapper above jersey client api to simplify test-specific configuration and validation + (jersey api is still available). The request builder unifies all possible configurations in one place. + - New common base client class TestClient + - ClientSupport is a TestClient, but also could provide 3 special clients: appClient(), adminClient(), restClient() + (restClient() is the same as rest stubs RestClient) + - New sub clients could be created by applying additional path segments: + client.subClient("/sub/path/) + - External api client could be created with support.externalClient("http://external.com/") + - New client rest api based on real method calls: restClient(RestClass.class).method(mock -> mock.restMethod(args)).invoke() + (target path and method type resolved from annotations, arguments used for request configuration) + - Helper api for testing multipart requests: restClient(..).multipartMethod(..) + (simplifies multipart method arguments creation) + - Defaults mechanism: it is possible to declare default headers, cookies, etc. + on the client to be applied for all requests (evolution of StubRest client ideas) + - Add PATCH method shortcuts + - Add builder-style response assertions like: client.do_request.assertHeader("Name", val) + This allows checking response headers, cookies, status code, etc. in a chained calls style without additional variables + - Add connector switching api to ClientSupport: apacheClient(), urlconnectorClient() + This allows using different connectors within one test + (note that apache connector is required for PATCH calls and urlconnector better handles multipart requests) + - Client logging now logs multipart requests (now PAYLOAD_ANY, before it was PAYLOAD_TEXT) + - (BREAKING) previous target(String... path) methods replaced with string format: target(String, Object...args) + - (BREAKING) deprecated targetMain(): replaced with targetApp() + - (BREAKING) StubRest default status declaration removed as not useful + (required status could be declared now with the new request builder) +* Add test web client field injection: + - @WebClient for ClientSupport, @WebClient(App), @WebClient(Admin), @WebClient(Rest) for specific clients + - @WebResourceClient for resource client direct mapping (works for integration and stub rest tests) +* Add guicey event ApplicationStartingEvent thrown just before managed and web services startup +* Fix stubs rest too early startup, causing problems with jersey registrations in application run method + +### [7.2.1](http://xvik.github.io/dropwizard-guicey/7.2.1) (2025-05-12) +* Fix NoClassDefFoundError on guicey startup due to junit classes leak into core (#428) + +### [7.2.0](http://xvik.github.io/dropwizard-guicey/7.2.0) (2025-05-11) +* Update to dropwizard 4.0.13 +* Un-deprecate HK2 support (removed deprecation annotations, but soft deprecation message remain in javadoc) +* Add methods to the main builder (and hooks) to simplify usage without guicey bundle: + - .whenConfigurationReady(...) - delayed configuration (same as GuiceyBunle.run): + simplify extensions or guice modules registration, requiring configuration + - .onGuiceyStartup() - executes after injector creation (under run phase). + Useful for manual dropwizard configurations + - .onApplicationStartup() - executes after complete application startup (including guicey lightweight test) + - .onApplicationShutdown() - executes after application shutdown + - .listenServer() - shortcut for jetty server startup listen + - .listenJetty() - shortcut for jetty lifecycle listening + - .listenJersey() - shortcut for jersey startup events and requests listening +* Diagnostic reports: + - Add application startup (and shutdown) time detalization report: .printStartupTime() + * Add hook alias for showing report on compiled application: -Dguicey.hooks=startup-time + - Add guice provision time report (time of guice beans creation): .printGuiceProvisionTime() + * Add hook alias for showing report on compiled application: -Dguicey.hooks=provision-time + * GuiceProvisionTimeHook could be used in tests to record beans creation at runtime + - Add the shared state usage report: .printSharedStateUsage() + - Improve guice bindings report (.printGuiceBindings()): + * Fixed scope accuracy for linked bindings + * Fixed bindings for private modules (missed exposed linked bindings) +* Guicey bundles: + - Add "throws Exception" for GuiceyBundle#initialize() to simplify usage + - Support extensions registration in GuiceyBundle run (.extensions() and .extensionsOptional()) + * ManualExtensionsValidatedEvent moved from configuration into run phase + * As before, classpath scan performed under configuration phase (but actual extensions registration moved to run phase) + - Transitive guicey bundles (.bundles(...)) initialize immediately after registration (unify behavior with dropwizard bundles and guice modules) + - Add onApplicationShutdown() and listenJersey() listener methods for GuiceyEnvironment (GuiceyBundle.run) +* Add "throws Exception" for GuiceyConfigurationHook#configure() to simplify usage +* Private guice modules support: + - Add private modules analysis: extensions searched in private module bindings too + (also important for avoiding duplicate binding registration after classpath scan) + - Add AnalyzePrivateGuiceModules option (enabled by default) to disable private modules + analysis (in case of problems) + - Disabled modules remove would also affect private modules now (but only first level) +* Classpath scan: + - Add extensions scan filters: GuiceBundle.builder().autoConfigFilter(cls -> !cls.isAnnotationPresent(Skip.class)) + Could be used either to skip some classes from scanning (without @InvisibleForScanner) annotation + or to accept only annotated classes (spring style) (#419) + * Added ClassFilters utility with common predicates: .autoConfigFilter(ignoreAnnotated(Skip.class)) + - Scan could detect package-private and protected extensions with a new option: + GuiceyOptions.ScanProtectedClasses (by default, false) (#404) +* Improve disable extensions predicate (bundle.disable(...)): + - Fix predicate applied for extension too early (without installer set) + - Add disable predicates: Disables.jerseyExtension, Disabled.webExtension and Disables.installedBy + - Predicates for exact type (module, bundle etc.) in Disables now raise item type to simplify further declarations +* Shared state: + - (breaking) Tie a state key to the stored object type to simplify usage (type-safe) and force + state objects usage instead of whatever values + - Fix null value supplier behavior (not allowed): .get(key, supplier) + - Add Options (read only accessor) object: state.getOptions() + - Add .whenReady() method for reactive state value access + * Add .whenSharedStateReady() for GuiceyBootstrap and GuiceyEnvironment + (not required for the main bundle as there is withSharedState() method where whenReady() could be used directly) + - Shared state usage report could be obtained at any time directly from the shared state + object (sharedState.getAccessReport()) +* Tests: + - Add the ability to disable managed objects lifecycle for lightweight guicey tests + (start/stop methods on managed objects not called; might be useful for tests with mocks): + * new GuiceyTestSupport().disableManagedLifecycle() + * @TestGuiceyApp(.., managedLifecycle = false) + * TestGuiceyAppExtension.forApp(..).disableManagedLifecycle() + * TestSupport.build(App.class).runCoreWithoutManaged(..) + - Add manual configuration object creation support for junit 5 extensions registered in field (@EnableSetup) + and TestEnvironmentSetup: .config(() -> {...}) + - Add missed configOverride(key, value) method for a single key-value pair + - Add configuration modifiers (`ConfigModifier`) - an alternative for configuration override mechanism: + ability to modify configuration instance before application startup. + Supported by all test extensions (junit5 annotations, setup object, generic builders, command runner) + - Add custom configuration block for junit5 extensions and TestEnvironmentSetup (to simplify lambda-based configurations): .with({...}) + - Junit ExtensionContext object could be injected as test method parameter + - Debug option: + * Track guicey test extensions time (would appear when debug enabled) + * Improve debug report: setup objects and hooks registration point are clear now (with direct code links) + - Add injectOnce option into test extensions to call injectMembers once per test instance + (useful when TestInstance.Lifecycle.PER_CLASS used) (discussion #394) + - Setup objects (TestEnvironmentSetup): + * Add "throws Exception" for TestEnvironmentSetup#setup() to simplify usage + * TestExtension builder improvements (TestEnvironmentSetup#setup(TestExtension)): + - Add getJunitContext() method to be able to configure test application with full context access (discussion #388) + - Add test lifecycle listeners: could be registered with listen() method or lambda-based on* methods + and provide notifications for guicey extension lifecycle (app start/stop, before/after test). + This is a simple alternative to writing junit extensions for additional integrations (db, testcontainers etc.). + - Add junit extension debug state method isDebug() so setup objects could + show debug output when debug option is enabled on guicey extension + - Add shortcut method isApplicationStartedForClass() to simplify beforeAll/beforeEach extension lifecycle detection + - Add annotated fields search api: findAnnotatedFields(..) to simplify writing annotation-driven extensions + * Add automatic setup objects (TestEnvironmentSetup) loading with service loader (simplify plugging-in extensions) + * Add base class for annotated fields extensions: AnnotatedTestFieldSetup + Handles fields validation and value injection lifecycle, including proper nested tests support + (all new test extensions based on it) + - New field-based test extensions: + * Add test stub fields: @StubBean(Service.class) ServiceStub + (use guice modules override feature to replace existing service into stub) + * Add mockito mock support: @MockBean Service. Mock automatically created + and override real dependency (module overrides used) + * Add mockito spy support: @SpyBean Service. Spy automatically created + and "proxy" real service (using aop) + * Add service trackers: @TrackBean Tracker. Tracker records all service + methods execution and could provide recordings for test verification or print performance + stats. A simpler replacement for mockito stubs. It could be used with mocks, spies and stubs + * Add REST stub (@StubRest): ability to start rest (or part of rest services) under @TestGuiceyApp + (without starting full container; same as dropwizard's ResourceExtension) + * Add logs testing support (@RecordLogs): record required logs for validation (only logback) + - Add option to disable default (new) annotated fields extensions: useDefaultExtensions +* Internal: + - Add BeforeInit guicey event (the first point with available Bootstrap) + - Add WebInstaller marker interface to identify web extensions (extensions started with jersey) + +### [7.1.4](http://xvik.github.io/dropwizard-guicey/7.1.4) (2024-09-14) +* Update to dropwizard 4.0.8 + +### [7.1.3](http://xvik.github.io/dropwizard-guicey/7.1.3) (2024-03-31) +* Update to dropwizard 4.0.7 +* Fix guicey ApplicationShutdownEvent typo (#387) + +### [7.1.2](http://xvik.github.io/dropwizard-guicey/7.1.2) (2024-02-17) +* Update to dropwizard 4.0.6 + +### [7.1.1](http://xvik.github.io/dropwizard-guicey/7.1.1) (2024-01-08) +* Update to dropwizard 4.0.5 + +### [7.1.0](http://xvik.github.io/dropwizard-guicey/7.1.0) (2023-11-28) +* Update to dropwizard 4.0.4 +* Add qualifier annotations support for configuration properties binding: + any configuration property (any level), annotated with qualifier annotation, would be + directly bound with that qualifier. Core dropwizard objects could be qualified on overridden getter +* Test improvements: + - Junit 5 extensions could inject DropwizardTestSupport object itself as test method parameter + - ClientSupport: + * inner jersey client creation is customizable now with TestClientFactory implementation + (new attribute "clientFactory" in @TestGuiceyApp and @TestDropwizardApp) + * default factory would automatically configure: + - multipart feature if available in classpath (dropwizard-forms) + - direct console logging (to see requests and responses directly in console) + * New methods: + - basePathRoot - root url (only with port) + - get(), post(), delete(), put() - simple shortcut methods to perform basic operations relative to server root + - Context support object (DropwizardTestSupport) and client (ClientSupport) instances are accessible now statically + for both manual run (TestSupport) and junit extensions: TestSupport.getContext() and TestSupport.getContextClient() + - New generic builder for flexible DropwizardTestSupport object creation and run (when junit extension can't be used): + TestSupport.builder() (with lifecycle listeners support) + - TestSupport methods changes: + * Creation and run methods updated with config override (strings) support + * Add creation and run methods application class only (and optional overrides). + * Run methods without callback now return RunResult containing all objects, required for validation (for example, to examine config) + * Add captureOutput method to record console output for assertions + - Commands test support: + * TestSupport.buildCommandRunner() - builds runner for command execution + with the same builder options as in generic builder (TestSupport.builder(); including same configuration) + and user input support. + * Could be used to test application startup fail (without using system mocks) + +### [7.0.2](http://xvik.github.io/dropwizard-guicey/7.0.2) (2023-10-06) +* Update to dropwizard 4.0.2 + +### [7.0.1](http://xvik.github.io/dropwizard-guicey/7.0.1) (2023-07-05) +* Update to dropwizard 4.0.1 +* [jdbi] + - Fix jdbi 3.39 compatibility + - Avoid redundant transaction isolation level checks (extra queries) (#318) +* [gsp] + - Fix redirection to error page after direct template rendering fails + +### [7.0.0](http://xvik.github.io/dropwizard-guicey/7.0.0) (2023-05-14) +* Update to dropwizard 4 + - (breaking) Use jakarta namespace instead of javax (servlet, validation) +* Update to guice 7 (jakarta.inject namespace) + +### [6.1.0](http://xvik.github.io/dropwizard-guicey/6.1.0) (2023-05-14) +* Update to guice 6.0 + +### [6.0.0](http://xvik.github.io/dropwizard-guicey/6.0.0) (2023-04-02) +* Update to dropwizard 3 + - (breaking) Drop java 8 support +* Merged with guicey-ext modules repository: + - Ext modules version would be the same as guicey + - dropwizard-guicey POM would not be a BOM anymore (everything moved to guicey-bom) + - Exclusions not applied in BOM anymore, instead they applied directly in POM + +### [5.7.1](http://xvik.github.io/dropwizard-guicey/5.7.1) (2023-03-09) +* Update to dropwizard 2.1.5 +* Revert changing reports log level: now INFO used instead of WARN (#276) + +### [5.7.0](http://xvik.github.io/dropwizard-guicey/5.7.0) (2022-12-29) +* Update to dropwizard 2.1.4 +* Fix NoClassDefFoundError(AbstractCollectionJaxbProvider) appeared for some jersey provider registrations (#240) +* Jersey extensions might omit `@Provider` on known extension types (ExceptionMapper, MessageBodyReader, etc.). + Unifies usage with pure dropwizard (no additional `@Provider` annotation required). (#265) + - New option InstallerOptions.JerseyExtensionsRecognizedByType could disable new behaviour +* Support ModelProcessor jersey extension installation (#186) +* Add extensions help: .printExtensionsHelp() showing extension signs recognized by installers (in recognition order) + - Custom installers could participate in report by overriding FeatureInstaller.getRecognizableSigns() + (default interface method). +* Change reports log level from INFO to WARN to comply with default dropwizard level +* Support application reuse between tests (#269) + - new reuseApplication parameter in extensions enables reuse + - reusable application must be declared in base test class: all tests derived + from this base class would use the same application instance +* Add SBOM (json and xml with cyclonedx classifier) +* Add .enableAutoConfig() no-args shortcut for enabling classpath scan in application package + +### [5.6.1](http://xvik.github.io/dropwizard-guicey/5.6.1) (2022-07-02) +* Update dropwizard to 2.1.1 (fixes java 8 issue by allowing afterburner usage) +* Fix classpath scan recognition of inner static classes inside jars (#231) +* Junit 5 extensions: + - Fix parallel test methods support (configuration overrides were applied incorrectly) + - Add "debug" option: when enabled, prints registered setup objects, hooks and + applied configuration overrides + * Setup objects and hooks not printed by default as before, only when debug enabled + * Debug could be also enabled with system property -Dguicey.extensions.debug=true + or with alias TestSupport.debugExtensions() + ### [5.6.0](http://xvik.github.io/dropwizard-guicey/5.6.0) (2022-06-07) * Update dropwizard to 2.1.0 * Test support objects changes: - Add new interface TestEnvironmentSetup to simplify test environment setup * In contrast to guicey hooks, setup objects used only in tests to replace the need of writing - additional junit extensions (for example, to setup test db). It provides a simple way to + additional junit extensions (for example, to set up a test db). It provides a simple way to override application configuration (e.g. to specify credentials to just started db) * Registration is the same as with hooks: annotation or inside extension builder and with field using new annotation @EnableSetup @@ -192,8 +463,8 @@ * extensions registered directly (or found by classpath scan) and also bound manually in guice module will not conflict anymore (as manual declaration would be detected) and so @LazyBinding workaround is not needed * extensions declared in guice module may be also disabled (guicey will remove binding declaration in this case - and all chains leading to this declartion to prevent possible context failures) - * Transitive gucie modules (installed by other modules) may be disabled with usual `disableModules()` + and all chains leading to this declaration to prevent possible context failures) + * Transitive guice modules (installed by other modules) may be disabled with usual `disableModules()` (but only if guice bindings analysis is not disabled). * enabled by default, but can be disabled with `GuiceyOptions.AnalyzeModules` option * `BindingInstaller` interface changed (because of direct guice bindings): @@ -228,7 +499,7 @@ Reporters are no more bound to guice context (they could always be constructed manually). * `DebugGuiceyLifecycle` listener renamed into `LifecycleDiagnostic` * Guicey reports (listeners) properly implement equals and hashcode in order to - use new deduplicatation mechanism and avoid reports duplication (for example, + use new deduplication mechanism and avoid reports duplication (for example, if `.printDiagnosticInfo()` would be called multiple times, only one report would be shown; but still different configurations will be reported separately (e.g. list `.printDiagnosticInfo()` and `.printAvailableInstallers()` which internally use one listener)) @@ -375,13 +646,13 @@ to enable bridge (#28) - WebFilterInstaller installs filters annotated with java.servlet.annotation.WebFilter - WebServletInstaller installs servlets annotated with java.servlet.annotation.WebServlet - WebListenerInstaller installs filters annotated with java.servlet.annotation.WebListener -* Add general options mechanism. Used to generify core guicey options, provide runtime options access (for bundles and reporting) and allow 3rd party bundles use it's own low-level options. +* Add general options mechanism. Used to generify core guicey options, provide runtime options access (for bundles and reporting) and allow 3rd party bundles use its own low-level options. - GuiceyBootstrap option(option) method provides access to defined options from bundles - Options guice bean provide access to options from guice services - Installers could access options by implementing WithOptions interface - OptionsInfo guice bean used for accessing options metadata (also accessible through GuiceyConfigurationInfo.getOptions()) - Options reporting added to DiagnosticBundle -* (breaking) remove GuiceBunldle methods: searchCommands(boolean), configureFromDropwizardBundles(boolean), bindConfigurationInterfaces(boolean) +* (breaking) remove GuiceBundle methods: searchCommands(boolean), configureFromDropwizardBundles(boolean), bindConfigurationInterfaces(boolean) (use either shortcuts without parameters or generic options method instead) * (breaking) core installers bundle now always installed (for both auto scan and manual modes). May be disabled with GuiceyOptions.UseCoreInstallers option * (breaking) configuration info api (GuiceyConfigurationInfo.getData()) changed to use java8 Predicate instead of guava @@ -429,7 +700,7 @@ NOTE: if used FeaturesHolder (internal api bean), now it's renamed to Extensions - Default: check 'guicey.bundles' system property and install bundles described there. May be useful for tests to enable debug bundles. - Default: use ServiceLoader mechanism to load declared GuiceyBundle services. Useful for automatic loading of third party extensions. - Add builder bundleLookup method to register custom lookup implementation - - Add builder disableBundleLookup to disable default lookups + - Add builder disableBundleLookup to disable default look-ups - Default lookup implementation logs all resolved bundles * Fix JerseyProviderInstaller: prevent HK2 beans duplicate instantiations; fix DynamicFeature support. * Add HK2DebugBundle. When enabled, checks that beans are instantiated by guice only and annotated with @HK2Managed diff --git a/src/doc/docs/about/license.md b/dropwizard-guicey/src/doc/docs/about/license.md similarity index 95% rename from src/doc/docs/about/license.md rename to dropwizard-guicey/src/doc/docs/about/license.md index 71cbcac7a..cb76e5fd2 100644 --- a/src/doc/docs/about/license.md +++ b/dropwizard-guicey/src/doc/docs/about/license.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2022, Vyacheslav Rusakov +Copyright (c) 2014-2025, Vyacheslav Rusakov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/dropwizard-guicey/src/doc/docs/about/migration.md b/dropwizard-guicey/src/doc/docs/about/migration.md new file mode 100644 index 000000000..1af26a192 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/about/migration.md @@ -0,0 +1,75 @@ +# Migration guide + +## Dropwizard 5.0 + +* [dropwizard upgrade instructions](https://www.dropwizard.io/en/stable/manual/upgrade-notes/upgrade-notes-5_0_x.html) + +Java 17 required. + +## Dropwizard 4.0 + +Migration to jakarta namespace (`jakarta.inject`, `jakarta.servlet`, `jakarta.persistence` instead of `javax.*`). + +* [dropwizard upgrade instructions](https://www.dropwizard.io/en/release-4.0.x/manual/upgrade-notes/upgrade-notes-4_0_x.html) + +Guice 7.0 [drops javax.* support completely](https://github.com/google/guice/wiki/Guice700), so you should either migrate +to guice annotations (like `com.google.guice.Inject`) or use jakarta annotations (like `jakarta.inject.Inject`). + +If you're upgrading from dropwizard 2.1 it is recommended to perform step-by-step migration (due to many breaking changes): + +* guicey 5.9.0 - dropwizard 2.1, guice 6 (changed guicey project structure - same as in guicey 6) +* guicey 6.1.0 - dropwizard 3, guice 6 (changed core dropwizard packaged) +* guicey 7.0.0 - dropwizard 4, guice 7 + +There might be problems with 3rd party guice libraries still using javax annotations - they would not work as planned +if `javax.inject` annotations used. If possible, migrate such libraries to jakarta namespace or, at least, +use guice native annotations (so library could work with all guice versions). + +As the last option, there is a [gradle plugin](https://github.com/nebula-plugins/gradle-jakartaee-migration-plugin) +for automatic conversion of project dependencies from javax to jakarta. This way, application started from gradle project +would use repackaged dependencies with correct jakarta namespace. Application delivery would also contain +custom (repackaged) jars. + +Using this plugin, I did initial [automatic guice migration](https://github.com/xvik/guice-jakartaee), (appeared before official +guice 7 with native jakarta support). You can use this project as an example of 3rd party library +repackage. + + +## Dropwizard 3.0 + +Java 8 support dropped! Many core packages were changed so there might be problems with 3rd party modules. + +* [dropwizard upgrade instructions](https://www.dropwizard.io/en/release-4.0.x/manual/upgrade-notes/upgrade-notes-3_0_x.html) + +* Guicey core was merged with ext modules to unify versioning. +* Examples repository was also merged into the [main repository](https://github.com/xvik/dropwizard-guicey/tree/master/examples) +* There is only one BOM now: `ru.vyarus.guicey:guicey-bom`. +* Dropwizard-guicey POM is not a BOM anymore (removing ambiguity). POM simplified by using direct exclusions instead of relying on BOM. + +!!! note + Guicey 5.8.0 (for dropwizard 2.1) applies the same project structure as in guicey 6 (dropwizard 3) and + so you can use it as the first migration step. + +## Dropwizard 2.1 + +* [dropwizard upgrade notes](https://www.dropwizard.io/en/release-4.0.x/manual/upgrade-notes/upgrade-notes-2_1_x.html) + +Since dropwizard 2.1.0 [jackson blackbird](https://github.com/FasterXML/jackson-modules-base/tree/jackson-modules-base-2.13.3/blackbird#readme) +[used by default](https://www.dropwizard.io/en/release-2.1.x/manual/upgrade-notes/upgrade-notes-2_1_x.html#jackson-blackbird-as-default) +instead of [afterburner](https://github.com/FasterXML/jackson-modules-base/tree/jackson-modules-base-2.13.3/afterburner#readme). +If you use **java 8** then apply afterburner dependency in order to switch into it: + +``` +implementation 'com.fasterxml.jackson.module:jackson-module-afterburner:2.13.3' +``` + +(omit version if guicey or dropwizard if BOM used). +Without it, you'll always see a nasty warning on startup (afterburner is better for java 8, but for java 9+ blackbird should be used) + +* [Java 8 issue discussion](https://github.com/xvik/dropwizard-guicey/discussions/226) +* [dropwizard upgrade instructions](https://www.dropwizard.io/en/release-2.1.x/manual/upgrade-notes/upgrade-notes-2_1_x.html) + +## Dropwizard 2.0 + +* [dropwizard upgrade instructions](https://www.dropwizard.io/en/release-2.0.x/manual/upgrade-notes/upgrade-notes-2_0_x.html) +* [guicey migration guide](http://xvik.github.io/dropwizard-guicey/5.0.0/about/release-notes/#migration-guide). diff --git a/dropwizard-guicey/src/doc/docs/about/release-notes.md b/dropwizard-guicey/src/doc/docs/about/release-notes.md new file mode 100644 index 000000000..b04c0b984 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/about/release-notes.md @@ -0,0 +1,869 @@ +# 8.0.0 Release Notes + +* Dropwizard 5 (requires java 17) +* [Guice without bundled ASM](#guice-without-bundled-asm) +* [Application fields injection](#application-fields-injection) +* [Auto close object stored in the shared state](#auto-close-object-stored-in-the-shared-state) + + +* Tests support: + - [Apache test client](#apache-test-client) + - [Unify ClientSupport and stubs rest](#unify-clientsupport-and-stubs-rest) + - [Test client fields support](#test-client-fields-support) + - [Application urls builder](#application-urls-builder) + - [Fix stubs rest early initialization](#fix-stubs-rest-early-initialization) + +[Migration guide](#migration-guide) + +## Guice without bundled ASM + +Guicey now use guice with a [classes](https://repo1.maven.org/maven2/com/google/inject/guice/7.0.0/) classifier without bundled ASM. +ASM is provided as a direct dependency. + +Required to support recent java versions (>22). + +## Application fields injection + +It is now possible to use injections in the application class. +Useful for accessing services in the application run method: + +```java +public class App extends Application { + @Inject Service service; + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + service.doSomething(); + } +} +``` + +## Apache test client + +`DefaultTestClientFactory` class was reworked to support extensibility. +`ApacheTestClientFactory` added (extending default factory) to use apache client (`Apache5ConnectorProvider`) +instead of urlconnection (`HttpUrlConnectorProvider`). + +!!! important "Why apache client is not set as default" + Apache client has [problems with multipart requests](https://github.com/eclipse-ee4j/jersey/issues/5528#issuecomment-1934766714) + (not a bug, but technical limitation). + + Urlconnection client has problems with PATCH requests on java > 16 + (requires additional --add-opens). + + Urlconnection remains as default because PATCH requests are less usable + than multipart forms. + +There are new shortcuts to switch to apache client in test: + +```java +@TestGuiceyApp(value=App.class, apacheClient=true) +``` + +Also, `ClientSupport` class now provides a shortcut to quickly switch client type +withing a test: + +```java +public void testSomething(ClientSupport client) { + ClientSupport apacheClient = client.apacheClient(); + ClientSupport urlConnectionClient = client.urlConnectionClient(); +} +``` + +## Unify ClientSupport and stubs rest + +Guicey provides default (universal) test client `ClientSupport` (used for integration tests) and +stubs rest (`@StubRest`) `RestClient` (used for lightweight rest tests). + +Now base client methods are unified by the `TestClient` class (extended by both): +this means both clients provide exactly the same request-related methods. + +Also, as `ClientSupport` represents an application root client, which is not +very useful in tests, it now provides 3 additinoal clients: + +* `.appClient()` - application context client (could not be the same as application root) +* `.adminClient()` - admin context client +* `.restClient()` - rest context client + +Plus, there is an ability to create a client for an external api: `.externalClient("http://api.com")` + +With it, you can use the same client request methods for different contexts (different base urls). +For example: + +```java +public void testSomething(ClientSupport client) { + User user = client.restClient().get("/users/123", User.class); +} +``` + +### Client API changes + +The main change is in `ClientSupport.target()` methods, which were previously +supporting a sequence of strings: `target(String... pathParts)`, but now +`String.format()` is used: `target(String format, Object... args)`. + +!!!! note "Why String.format?" + Sequence of paths appears to be a bad idea. Compare: + `target("a", "12", "c")` and `target("a/%s/c", 12)`. + The latter is more readable and more flexible. + +Moreover, all shortcut methods now support `String.format()` syntax too: +`get("/users/%s", 123)` and `post("/users/%s", new User(), 123)` + +There is now not only `Class`-based shortcuts, but also a `GenericType`-based: + +```java + Uset user = client.get("/users/%s", User.class, 123); + List list = client.get("/users", new GenericType<>() {}); +``` + +As an addition to existing method shortcuts (`get()`, `post()`, etc), +there is a new `patch()` shortcut: `client.patch("/users/%s", new User(), 123)` + +!!! WARNING "Breaking changes" + * Due to `String.format()`-based syntax, old calls like `target("a", "12", "c")` will work incorrectly now. + * It was possible to use target method without arguments to get root path: `target()`, + but now it is not possible. Use `target("/")` instead. + * Old calls like `client.get("/some/path", null)` used for void calls will also not work + (jvm will not know what method to choose: Class or GenericType). Instead, there is a special void shortcut now: + `client.get("/some/path")` (but `client.get("/some/path", Void.class)` will also work). + * `basePathMain()` and `targetMain()` methods now deprecated: use `basePathApp()` and `targetApp()` instead. + +### Default logger change + +By default, all `ClientSupport` (and stubs `RestClient`) requests are logged, +but before it was not logging multipart requests. + +Now the logger is configured to log everything by default, including multipart requests. + +### Request defaults + +The initial concept of request defaults was introduced in stubs rest `RestClient`. +Now it evolved to be a universal concept for all clients. + +Each `TestClient` provide "default*" methods to set request defaults: + +* `defaultHeader("Name", "value")` +* `defaultQueryParam("Name", "value")` +* `defaultCookie("Name", "value")` +* `defaultAccept("application/json")` +* etc. + +The most obvious use case is authorization: + +```java +public void testSomething(ClientSupport client) { + client.defaultHeader("Authorization", "Bearer 123"); + + User user = client.restClient().get("/users/123", User.class); +} +``` + +### Sub clients + +There is a concept of sub clients. It is used to create a client for a specific sub-url. +For example, suppose all called methods in test have some base path: `/{somehting}/path/to/resource`. +Instead of putting it into each request: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient(); + + rest.get("/%s/path/to/resource/%s", User.class, "path", 12); + rest.post("/%s/path/to/resource/%s", new User(...), "path", 12); +} +``` + +A sub client can be created: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient().subClient("/{something}/path/to/resource") + .defaultPathParam("something", "path"); + + rest.get("/%s", User.class, "path", 12); + rest.post("/%s", new User(...), "path", 12); +} +``` + +!!! note + Sub clients inherit defaults of parent client. + + ```java + client.defaultQueryParam("q", "v"); + TestClient rest = client.subClient("/path/to/resource"); + + // inherited query parameter q=v will be applied to all requests + rest.get("/%s", User.class, 12); + ``` + +Defaults could be cleared at any time with `client.reset()`. + +There is a special sub client creation method using jersey `UriBuilder`, required +to properly support matrix parameters in the middle of the path: + +```java +TestClient sub = client.subClient(builder -> builder.path("/some/path").matrixParam("p", 1)); + +// /some/path;p=1/users/12 +sub.get("/%s", User.class, 12); +``` + +### New builder API + +There is a new builder API for `TestClient` covering all possible configurations +for jersey `WebTarget` and `Invocation.Builder`. The main idea was to simplify +request configuration: to provide all possible methods in one place. + +For example: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .as(User.class) +``` + +Request specific extensions and properties are also supported: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .register(VoidBodyReader.class) + .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) + .asVoid(); +``` + +All builder methods start with a "build" prefix (`buildGet()`, `buildPost()` or generic `build()`). + +Builder provides direct value mappings: + +* `.as(Class)` +* `.as(GenericType)` +* `.asVoid()` +* `.asString()` + +And methods, returning raw (wrapped) response: + +* `.invoke()` - response without status checks +* `.expectSuccess()` - fail if not success +* `.expectSuccess(201, 204)` - fail if not success or not expected status +* `.expectRedirect()` - fail if not redirect (method also disabled redirects following) +* `.expectRedirect(301)` - fail if not redirect or not expected status +* `.expectFailure()` - fail if success +* `.expectFailure(400)` - fail success or not expected status + +Response wrapper would be described below. + +### Debugging + +Considering the client defaults inheritance (potential decentralized request configuration) +it might be unobvious what was applied to the request. + +Request builder provides a `debug()` option, which will print all applied defaults +and direct builder configurations to the console: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .debug() + .as(User.class) +``` + +``` +Request configuration: + + Path params: + p1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:61) + p2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + p3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + + Query params: + q1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:57) + q2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + q3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + + Accept: + application/json at r.v.d.g.t.c.builder.(RequestBuilderTest.java:54) + +Jersey request configuration: + + Resolve template at r.v.d.g.t.c.builder.(TestRequestConfig.java:869) + (encodeSlashInPath=false encoded=true) + p1=1 + p2=2 + p3=3 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q1=1 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q2=2 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q3=3 + + Accept at r.v.d.g.t.c.builder.(TestRequestConfig.java:899) + [application/json] +``` + +It shows two blocks: + +* How request builder was configured (including defaults source) +* How jersey request was configured + +The latter is obtained by wrapping jersey `WebTarget` and `Invocation.Builder` +objects to intercept all calls. + +Debug could be enabled for all requests: `client.defaultDebug(true)`. + +### Request assertions + +It would not be very useful for the majority of cases, but as debug api could +aggregate all request configuration data, it is possible to assert on it: + +```java +client.buildGet("/some/path") + .matrixParam("p1", "1") + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("/some/path;p1=1")) + .as(SomeEntity.class); +``` + +or + +```java +.assertRequest(tracker -> assertThat(tracker.getQueryParams().get("q")).isEqualTo("1")) +``` + +### Form builder + +There is a special builder helping build urlencoded and multipart requests (forms): + +```java +// urlencoded +client.buildForm("/some/path") + .param("name", 1) + .param("date", 2) + .buildPost() + .as(String.class); + +// multipart +client.buildForm("/some/path") + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildPost() + .asVoid(); +``` + +Also, it could be used to simply create a request entity and use it directly: + +```java +Entity entity = client.buildForm(null) + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildEntity() + +client.post("/some/path", entity); +``` + +Builder will serialize all provided (non-multipart) parameters to string. +For dates, it is possible to specify a custom date format: + +```java +client.buildForm("/some/path") + .dateFormat("dd/MM/yyyy") + .param("date", new Date()) + .param("date2", LocalDate.now()) + .buildPost() + .asVoid(); +``` + +(java.util and java.time date formatters could be set separately with `dateFormatter()` or `dateTimeFormatter()` methods) + +The default format could be changed globally: `client.defaultFormDateFormat("dd/MM/yyyy")` +(or `defaultFormDateFormatter()` with `defaultFormDateTimeFormatter()`). + +### Response assertions + +As mentioned above, request builder method like `.invoke()` or `.expectSuccess()` returns +a special response wrapper object. It provides a lot of useful assertions to simplify +response data testing (avoid boilerplate code). + +For example, check a response header, cookie and obtain value + +```java +User user = rest.buildGet("/users/123") + .expectSuccess() + .assertHeader("Token" , s -> s.startsWith("My-Header;")) + .assertCookie("MyCookie", "12") + .as(User.class); +``` + +Here assertion error will be thrown if header or cookie was not provided or condition does not match. + +Redirection correctness could be checked as: + +```java +@Path("/resources") +public class Resource { + + @Inject + AppUrlBuilder urlBuilder; + + @Path("/list") + @GET + public Response get() { + ... + } + + @Path("/redirect") + @GET + public Response redirect() { + return Response.seeOther( + urlBuilder.rest(SuccFailRedirectResource.class).method(Resource::get).buildUri() + ).build(); + } +} +``` + +```java +rest.method(Resource::redirect) + // throw error if not 3xx; also, this disables redirects following + .expectRedirect() + .assertHeader("Location", s -> s.endsWith("/resources/list")); +``` + +Also, "with*" methods could be used for completely manual assertions: + +```java +rest.method(Resource::redirect) + .expectSuccess(201) + .withHeader("MyHeader", s -> + assertThat(s).startsWith("My-Header;")); +``` + +Response object could be converted without additional variables: + +```java +String value = rest.method(Resource::redirect) + .expectSuccess() + .as(res -> res.readEntity(SomeClass.class).getProperty()); +``` + +### Jersey API + +As before, it is possible to use `client.target("/path")` to build raw jersey target +(with the correct base path). But without applied defaults. + +Direct `Invocation.Builder` could be built with `client.request("/path")`. +Here all defaults would be applied. + +Builders does not hide native jersey API: + +* `WebTarget` - could be modified directly with `request.configurePath(target -> target.path("foo"))` +* `Invocation.Builder` - with `request.configureRequest(req -> req.header("foo", "bar"))` + +Such modifiers could be applied as client defaults: + +* `client.defaultPathConfiguration(...)` +* `client.defaultRequestConfiguration(...)` + +Response wrapper also provides direct access to jersey `Response` object: +`response.asResponse()`. + +### Resource clients + +There is a special type of type-safe clients based on the simple idea: +resource class declaration already provides all required metadata to configure a test request: + +```java +@Path("/users") +public class UserResource { + + @Path("/{id}") + @GET + public User get(@NotNull @PathParam("id") Integer id) {} +} +``` + +Resource declares its path in the root `@Path` annotation and method annotations +tell that it's a GET request on path `/users/{id}` with required path parameter. + +```java +public void testSomething(ClientSupport client) { + // essentially, it's a sub client build with the resource path (from @Path annotation) + ResourceClient rest = client.restClient(UserResource.class); + + User user = rest.method(r -> r.get(123)).as(User.class); +} +``` + +By using a mock object call (`r -> r.get(123)`) we specify a source of metadata and the required values +for request. Using it, a request builder is configured automatically. + +It is not required to use all parameters (reverse mapping is not always possible): +use null for not important arguments. All additional configurations could be done manually: + +```java +public void testSomething(ClientSupport client) { + ResourceClient rest = client.restClient(UserResource.class); + + User user = rest.method(r -> r.get(null)) + .pathParam("id", 123) + .as(User.class); +} +``` + +Almost everything could be recognized: + +* All parameter annotations like `@QueryParam`, `@PathParam`, `@HeaderParam`, `@MatrixParam`, `@FormParam`, etc. +* All request methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. +* Request body mapping: `void post(MyEntity entity)` +* And even multipart forms + +Not related arguments should be simply ignored: + +```java +public void get(@PathParam("id") Integer id, @Context HttpServletRequest request) {} + +rest.method(r -> r.get(123, null)); +``` + +!!! note + `ResourceClient` extends `TestClient`, so all usual method shortucts are also available for resource client + (real method calls usage is not mandatory). + +#### Multipart forms + +Multipart resource methods often use special multipart-related entities, like: + +```java + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) +``` + +Which is not handy to create manually. To address this, `ResourceClient` provides a +special helper object to build multipart-related values: + +```java +rest.multipartMethod((r, multipart) -> + r.multipart(multipart.fromClasspath("/sample.txt"), + multipart.disposition("file", "sample.txt")) + .asVoid()); +``` + +Here file stream passed as a first parameter and filename with the second one. + +Or + +```java + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2( + @NotNull @FormDataParam("file") FormDataBodyPart file) +``` + +```java + rest.multipartMethod((r, multipart) -> + r.multipart2(multipart.streamPart("file", "/sample.txt"))) + .asVoid(); +``` + +In case of generic multipart object argument: + +```java + @Path("/multipartGeneric") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartGeneric(@NotNull FormDataMultiPart multiPart) +``` + +there is a special builder: + +```java +rest.multipartMethod((r, multipart) -> + r.multipartGeneric(multipart.multipart() + .field("foo", "bar") + .stream("file", "/sample.txt") + .build())) + .as(String.class); +``` + +!!! note + Multipart methods require the urlencoded client (default) and, most likely, + will fail with the apache client. + +#### Sub resources + +When a sub resource is declared with an instance: + +```java +public class Resource { + @Path("/sub") + public SubResource sub() { + return new SubResource(); + } +} +``` + +it could be easily called directly: + +```java +User user = rest.method(r -> r.sub().get(123)).as(User.class); +``` + +When a sub resource method is using class: + +```java +public class Resource { + @Path("/sub") + public Class sub() { + return SubResource.class; + } +} +``` + +you'll have to build a sub-client first: + +```java +ResourceClient subRest = rest.subResource(Resource::sub, SubResource.class); +``` + +!!! important + Jersey ignores sub-resource `@Path` annotation, so special method for sub resource clients is required. + +#### Resource typification + +It is not always possible to use resource class to buld a sub client +(with `.restClient(Resource.class)`). + +In such cases you can build a resource path manually and then "cast" client to the resource type: + +```java +ResourceClient rest = client.subClient("/resource/path") + .asResourceClient(MyResource.class); +``` + +or just buid path manually: + +```java +ResourceClient rest = client.subClient( + builder -> builder.path("/resource").matrixParam("p", 123), + MyResource.class); +``` + +## Test client fields support + +Before, `ClientSupport` could only be injected as test method (or setup method) parameter. +parameter: + +```java +public void testSomething(ClientSupport client) +``` + +Now it is possible to inject it as a field: + +```java +@WebClient +ClientSupport client; +``` + +It is also possible to inject its sub clients: + +```java +@WebClient(WebClientType.App) +TestClient app; + +@WebClient(WebClientType.Admin) +TestClient admin; + +@WebClient(WebClientType.Rest) +TestClient rest; +``` + +Additionally, `ResourceClient` could be injected directly: + +```java +@WebResourceClient +ResourceClient rest; +``` + +!!! important + Resource client injection works both with integration tests (real client) + and stub rest (lightweight tests): + + ```java + @TestGuiceyApp(MyApp.class) + public class MyTest { + StubRest + RestClient client; + + @WebResourceClient + ResourceClient rest; + } + ``` + + Note that resource client could be directly obtained form `RestClient`: + `client.restClient(MyResource.class)` (same as in `ClientSupport`). + +!!! important + Clients assigned with a field would reset client defaults automatically (call `client.reset()`) after + each test method. This could be disabled with `@WebClient(autoReset = false)`. + +## Application urls builder + +Mechanism, used in resource clients, could be also used to build application urls in a type-safe manner. + +A new class `AppUrlBuilder` added to support this. It is not bound by default +in the guice context, but could be injected (as jit binding): + +```java +@Inject AppUrlBuilder builder; +``` + +Or it could be created manually: `new AppUrlBuilder(environment)` + +There are 3 scenarios: + +* Localhost urls: the default mode when all urls contain "localhost" and application port. +* Custom host: `builder.forHost("myhost.com")` when custom host used instead of localhost and application port + is applied automatically +* Proxy mode: `builder.forProxy("https://myhost.com")` when application is behind some proxy + (like apache or nginx) hiding its real port. + +Examples: + +```java +// http://localhost:8080/ +builder.root("/") +// http://localhost:8080/ +builder.app("/") +// http://localhost:8081/ +builder.admin("/") +// http://localhost:8080/ +builder.rest("/") + +// http://localhost:8080/users/123 +builde.rest(Resource.class).method(r -> r.get(123)).build() +// http://localhost:8080/users/123 +builde.rest(Resource.class).method(r -> r.get(null)).pathParam("id", 123).build() + + +// https://some.com:8081/something +builder.forHost("https://some.com").admin("/something") + +// https://some.com/something +builder.forProxy("https://some.com").admin("/something") +``` + +Before, application server configuration detection logic was only implemented inside `ClientSupport`, +now it was ported to `AppUrlBuilder`, which you can use to obtain: + +```java +// 8080 +builder.getAppPort(); +// 8081 +builder.getAdminPort(); +// "/" (server.adminContextPath) +builder.getAdminContextPath(); +// "/" (server.applicationContextPath) +builder.getAppContextPath(); +// "/" (server.rootPath) +builder.getRestContextPath(); +``` + +## Fix stubs rest early initialization + +`@StubRest` rest context was starting too early, causing closed configuration error +when `Application#run` method tried to configure it (`environment.jersey().register(Something.class)`). + +Now it is started after the application run. + +## Auto close object stored in the shared state + +`SharedConfigurationState` values, implementing `AutoCloseable` could be closed +automatically now after application shutdown. + +## Migration guide + +All breaking changes affect only test code (ClientSupport and RestClient). + +### Target methods + +Before: + +```java +public void testSomething(ClientSupport client) { + // use String... method + client.targetMain("/path/", 12, "part"); +} +``` + +Now: + +```java +// use String.format syntax +client.targetApp("/path/%s/part", 12); +``` + +Deprecations: + +* `targetMain` - use `targetApp` instead +* `basePathMain` - use `basePathApp` instead + +### Shortcuts + +Before, to receive a void response, you had to use: + +```java +client.post("/post", entity, null); +``` + +Now, because there are two variations of each shortcut method (with Class and GenericType), +java would not know which one to use: + +```java +// either use new shortcut +client.post("/post", entity); +// or use Void.class directly +client.post("/post", entity, Void.class); +``` + +### Default status + +Before, RestClient (stubs rest) provide a default status method `client.defaultOk` +to specify what statuses to treat as success: + +```java + +@StubRest +RestClient client; + +public void testSomething() { + rest.defaultOk(201); + + // error on 200 response (only 201 required) + rest.post("/some/path", entity, null); +} + +``` + +The same could be achieved now with a new builder: + +```java +rest.buildPost("/some/path", entity) + .expectSuccess(201); +``` + +Note that, by default, a jersey client assumes response as success if its status is 2xx +(by status family). This approach is more logical. Limitation to only some 2xx statuses +is rarely needed. diff --git a/dropwizard-guicey/src/doc/docs/about/support.md b/dropwizard-guicey/src/doc/docs/about/support.md new file mode 100644 index 000000000..1a8cf0fde --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/about/support.md @@ -0,0 +1,5 @@ +# Support + +* [Github issues](https://github.com/xvik/dropwizard-guicey/issues) - bug reports / enhancement requests +* [Github discussions](https://github.com/xvik/dropwizard-guicey/discussions) - questions / propositions / discussions +* [Gitter](https://app.gitter.im/#/room/#xvik_dropwizard-guicey:gitter.im) - chat diff --git a/src/doc/docs/concepts.md b/dropwizard-guicey/src/doc/docs/concepts.md similarity index 91% rename from src/doc/docs/concepts.md rename to dropwizard-guicey/src/doc/docs/concepts.md index 11ecc932a..a7587912e 100644 --- a/src/doc/docs/concepts.md +++ b/dropwizard-guicey/src/doc/docs/concepts.md @@ -12,7 +12,7 @@ HK2 context is launched too late (after dropwizard run phase) and, for example, impossible to use HK2 to instantiate dropwizard managed objects because managed must be registered before HK2 context starts. -Guicey use lazy factories for integration: it register providers for HK2 objects in +Guicey use lazy factories for integration: it registers providers for HK2 objects in guice context. Guice-managed objects (extensions) are simply registered as instances. So most of the time you don't have to know about HK2 at all. @@ -22,8 +22,12 @@ In this case you may require to explicitly register hk2-guice bride so hk2 could see guice beans directly. !!! danger - Since jersey 2.26 it is possible to get rid of HK2 completely. Next guicey version - will ONLY use guice and all current HK2-related features will be removed. + Since jersey 2.26 it is possible to get rid of HK2 completely. Unfortunately, such + update requires a lot of work. Someday guicey will ONLY use guice and all current + HK2-related features will be removed. + For now, all HK2-related features are softly deprecated: no deprecation annotations, + just mention in javadoc. + ## Lifecycle @@ -39,11 +43,13 @@ and **start injector on run phase**. If we create injector in initialization phase then we will not have access to `Configuration` and `Environment` in guice modules, but configuration could be required, especially for 3rd party modules, which does not support lazy configuration. - -The only exception for configuration under initialization phase is -guice modules, which can be registered in run phase (simply because modules too often -require configuration values for construction). As a consequence, extensions recognized -from guice bindings are registered in run phase too. + +It is preferred to register everything under the initialization phase, but not possible in many cases. +We still need to register guice modules, requiring configuration values for construction under run phase. + +In some cases, extension registration may depend on configuration value, and so +it is also allowed to register extensions in run phase (besides, extensions could be +recognized from guice bindings, registered at run phase). This separation of initialization and run phases makes configuration more predictable (especially important when bundles depend on initialization order). @@ -51,8 +57,8 @@ This separation of initialization and run phases makes configuration more predic ### Guice module In the main `GuiceBundle` guice modules registration appears under initialization phase (when -neither `Configuration` nor `Environment` objects are available). If module require these objects -and it's registration can't be moved to guicey bundle's run method, then use +neither `Configuration` nor `Environment` objects are available). If a module requires these objects +and its registration can't be moved to guicey bundle's run method, then use [marker interfaces](guide/guice/module-autowiring.md). For example, `ConfigurationAwareModule` will lead to configuration object set into module before injector creation. @@ -70,6 +76,8 @@ to configuration object set into module before injector creation. confuguration(Class) // unique sub configuration configuration(String) // configuration value by yaml path configurations(Class) // sub configuration objects by type (including subtypes) + annotatedConfiguration(ann) // annotaed configuration value by instance + annotatedConfiguration(Class) // annotaed configuration value by annotation type options() // access guicey options } } @@ -163,7 +171,7 @@ This force you to always use all request scoped objects through `Provider`. But, this avoids a jvm garbage from creating them for each request and makes everything a bit faster (no extra DI work required for each request). -If you think that developer comfort worth more then small performance gain, then: +If you think that developer comfort worth more than small performance gain, then: * You can use explicit scope annotations to change singleton scope (`@RequestScoped`, `@Prototype`) * Switch off forced singletons (`.option(InstallerOptions.ForceSingletonForJerseyExtensions, false)`) @@ -189,8 +197,7 @@ additional configurations (thanks to classpath scan). Another example is [`PluginInstaller`](installers/plugin.md) which allows you to declare plugins (e.g. implementing some interface) and inject all of them at once (as `Set`). -[Extensions project](https://github.com/xvik/dropwizard-guicey-ext) provides special installer to -[register events in guava eventBus](extras/eventbus.md): +[guicey-eventbus](extras/eventbus.md) provides special installer to register events in guava eventBus: `EventBusInstaller` check class methods and if any method is annotated with `@Subscribe` - register extension as event bus listener. @@ -206,7 +213,7 @@ and write installer to automatically register such classes in scheduler framewor ### Core installers override -It is also possible to replace any core installer (e.g. to change it's behaviour) - +It is also possible to replace any core installer (e.g. to change its behaviour) - you just need to disable core installer and install a replacement: ```java @@ -241,7 +248,7 @@ The concept is great, but, in context of guice, dropwizard bundle did not allow register guice modules (and, of course, guicey installers and extensions). So there is no way to elegantly re-use dropwizard bundles mechanism. -Guicey introduce it's own bundles: +Guicey introduce its own bundles: ```java public interface GuiceyBundle { @@ -270,11 +277,11 @@ Provides access to dropwizard configuration, environment and introspected config ### Bundles usage difference -In dropwizard bundles are helpful not just for extracting re-usable extensions, but for +In dropwizard, bundles are helpful not just for extracting re-usable extensions, but for separation of application logic. In guicey, you don't need to write registration code and with enabled [classpath scan](guide/scan.md), -don't need to configure much at all. This makes guicy bundles mostly usable for 3rd party integrations (or core modules extraction for large projects), +don't need to configure much at all. This makes guicey bundles mostly usable for 3rd party integrations (or core modules extraction for large projects), where you can't (and should not) rely on class path scan and must declare all installers and extensions manually. Many bundle examples could be found in [extension modules](guide/modules.md). @@ -302,7 +309,7 @@ implicitly registered. It *does not mean* guicey loads all bundles in classpath! !!! tip - ServiceLoader and property based lookups are always enabled, but you can switch them + ServiceLoader and property based look-ups are always enabled, but you can switch them off if required with `.disableBundleLookup()` bundle option. @@ -322,7 +329,7 @@ Suppose you have some 3rd party bundle: ```java public class XBundle implements GuiceyBundle { - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap .extensions(...) .modules(new XModule(), new XAddonModule()); @@ -397,14 +404,14 @@ bundle: ```java public class Feature1Bundle implements GuiceyBundle { - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.bundles(new CommonBundle()); ... } } public class Feature2Bundle implements GuiceyBundle { - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.bundles(new CommonBundle()); ... } @@ -440,8 +447,8 @@ GuiceBundle.builder() Note that "common bundle" problem for dropwizard bundles may be solved by simply registering dropwizard bundles through guicey api. -!!! warning "Dropwizrd bundles" - Only dropwizard bundles, registered through guciey api are visible. +!!! warning "Dropwizard bundles" + Only dropwizard bundles, registered through guicey api are visible. So if there would be one bundle registered directly in dropwizard and another with guicey api - guicey will not detect duplicate. @@ -530,7 +537,7 @@ There is a special group of `print[Something]` methods, which are intended to he you understand internal state (and help with debugging). As you have seen, real life configuration could be quite complex because you may have many extensions, observed with classpath scan, -bundles, bundles installing other bundles, many gucie modules. Also, some bundles +bundles, bundles installing other bundles, many guicey modules. Also, some bundles may disable extensions, installers, guice modules (and some modules could even override bindings). During startup guicey tracks all performed configurations and you can even access [this diff --git a/src/doc/docs/decomposition.md b/dropwizard-guicey/src/doc/docs/decomposition.md similarity index 90% rename from src/doc/docs/decomposition.md rename to dropwizard-guicey/src/doc/docs/decomposition.md index 8624350c8..ad0d2e302 100644 --- a/src/doc/docs/decomposition.md +++ b/dropwizard-guicey/src/doc/docs/decomposition.md @@ -36,7 +36,7 @@ Benefits: - guice support (ability to register guice modules) - [options](guide/options.md) support -- use [sub-configration objects](guide/yaml-values.md#unique-sub-configuration) directly (important for writing generic modules) +- use [sub-configuration objects](guide/yaml-values.md#unique-sub-configuration) directly (important for writing generic modules) - define custom [extension types](guide/extensions.md) to simplify usage (e.g. like [jdbi](extras/jdbi3.md)) - [automatic module loading](concepts.md#bundles-lookup) when jar appear in classpath (e.g. like [lifecycle annotations](extras/lifecycle-annotations.md)) - [shared state](guide/shared.md) - advanced techniques for bundle communication (e.g. used by [GSP](extras/gsp.md) and [SPA](extras/spa.md)) @@ -72,7 +72,7 @@ public class MyBundle implements ConfiguredBundle { ```java public class MyBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap .dropwizardBundles(new DwBundle()) .bundles(new MyOtherBundle()); @@ -82,7 +82,7 @@ public class MyBundle implements GuiceyBundle { } ``` -Guicey bundle de-duplication logic is further explaned [here](guide/deduplication.md). In short, registered root bundles +Guicey bundle de-duplication logic is further explained [here](guide/deduplication.md). In short, registered root bundles must be initialized in priority. This avoids situations like: ```java @@ -178,8 +178,8 @@ public class ModuleImpl extends DropwizardAwareModule new SomeState()); ... @@ -476,7 +517,7 @@ bundles, which use or append to this global state: ```java public class GlobalBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { // share global state object bootstrap.shareState(GlobalBundle, new GlobalState()); } @@ -484,7 +525,7 @@ public class GlobalBundle implements GuiceyBundle { public class ChildBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { // access shared object or fail when not found GlobalState state = environment.sharedStateOrFail(GlobalBundle, "Failed to obtain global state - check if global bundle registered"); diff --git a/src/doc/docs/dev.md b/dropwizard-guicey/src/doc/docs/dev.md similarity index 82% rename from src/doc/docs/dev.md rename to dropwizard-guicey/src/doc/docs/dev.md index 650c7b658..201aab77e 100644 --- a/src/doc/docs/dev.md +++ b/dropwizard-guicey/src/doc/docs/dev.md @@ -18,8 +18,11 @@ To enable automatic reload of static resources: classpath, which may be harmful for some applications. In case of dropwizard applications there should be no problems still (only with your custom logic dealing with classpath directly) + +* Advanced settings + - Click "Allow auto-make to start even if developed application is currently running" Now static resources would "hot swap". !!! warning - Note that template engines (fremmarker, mustache) may [cahce templates](https://www.dropwizard.io/en/latest/manual/views.html#caching) + Note that template engines (freemarker, mustache) may [cache templates](https://www.dropwizard.io/en/latest/manual/views.html#caching) diff --git a/src/doc/docs/examples/authentication.md b/dropwizard-guicey/src/doc/docs/examples/authentication.md similarity index 90% rename from src/doc/docs/examples/authentication.md rename to dropwizard-guicey/src/doc/docs/examples/authentication.md index 0350502ac..ad74be0ae 100644 --- a/src/doc/docs/examples/authentication.md +++ b/dropwizard-guicey/src/doc/docs/examples/authentication.md @@ -1,10 +1,10 @@ # Authentication -Example of [dropwizard authentication](https://www.dropwizard.io/en/release-2.0.x/manual/auth.html) usage with guice. +Example of [dropwizard authentication](https://www.dropwizard.io/en/release-4.0.x/manual/auth.html) usage with guice. ## Simple auth -Using [dropwizard oauth](https://www.dropwizard.io/en/release-2.0.x/manual/auth.html#oauth2) example as basement. +Using [dropwizard oauth](https://www.dropwizard.io/en/release-4.0.x/manual/auth.html#oauth2) example as basement. Other auth types are configured in similar way. ```java @@ -46,19 +46,19 @@ public class OAuthDynamicFeature extends AuthDynamicFeature { ``` The class is automatically picked up by the [jersey installer](../installers/jersey-ext.md#dynamicfeature). -`OAuthAuthenticator` and `OAuthAuthorizer` are simple guice beans (no special installation required). +`OAuthAuthenticator` and `UserAuthorizer` are simple guice beans (no special installation required). Constructor injection is used to obtain required guice managed instances and then configure authentication the same way as described in dropwizard docs. -If auto configuration is enabled, then the class will be resolved and installed automatically. +If autoconfiguration is enabled, then the class will be resolved and installed automatically. !!! note "" - Complete [OAuth example source](https://github.com/xvik/dropwizard-guicey-examples/tree/master/integration-auth) + Complete [OAuth example source](https://github.com/xvik/dropwizard-guicey/tree/master/examples/integration-auth) ## Chained auth -[Chained auth](https://www.dropwizard.io/en/release-2.0.x/manual/auth.html#chained-factories) can be used to support different authentication schemes. +[Chained auth](https://www.dropwizard.io/en/release-4.0.x/manual/auth.html#chained-factories) can be used to support different authentication schemes. Integration approach is the same as in simple case: @@ -92,7 +92,7 @@ public class ChainedAuthDynamicFeature extends AuthDynamicFeature { ## Polymorphic auth -[Polymorphic auth](https://www.dropwizard.io/en/release-2.0.x/manual/auth.html#multiple-principals-and-authenticators) allows using different auth schemes simultaneously. +[Polymorphic auth](https://www.dropwizard.io/en/release-4.0.x/manual/auth.html#multiple-principals-and-authenticators) allows using different auth schemes simultaneously. Integration approach is the same as in simple case: @@ -125,4 +125,3 @@ public class PolyAuthDynamicFeature extends PolymorphicAuthDynamicFeature { } } ``` - diff --git a/src/doc/docs/examples/eventbus.md b/dropwizard-guicey/src/doc/docs/examples/eventbus.md similarity index 92% rename from src/doc/docs/examples/eventbus.md rename to dropwizard-guicey/src/doc/docs/examples/eventbus.md index 1f22ec9d4..47fd6bb25 100644 --- a/src/doc/docs/examples/eventbus.md +++ b/dropwizard-guicey/src/doc/docs/examples/eventbus.md @@ -3,7 +3,7 @@ Example of [guicey-eventbus](../extras/eventbus.md) extension usage. !!! abstract "" - Example [source code](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-eventbus) + Example [source code](https://github.com/xvik/dropwizard-guicey/examples/tree/master/ext-eventbus) The [eventbus extension](../extras/eventbus.md) is used for: @@ -16,7 +16,7 @@ The [eventbus extension](../extras/eventbus.md) is used for: An additional dependency is required: ```groovy -implementation 'ru.vyarus.guicey:guicey-eventbus:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-eventbus:{{ gradle.version }}' ``` !!! note @@ -91,4 +91,4 @@ public void onMultipleEvents(BaseEvent event) {} or manually declared guice bean (using module) or bean created with guice AOT (because it's declared as dependency for other bean) will be searched for listener methods. -See [a complete example](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-eventbus) \ No newline at end of file +See [a complete example](https://github.com/xvik/dropwizard-guicey/examples/tree/master/ext-eventbus) \ No newline at end of file diff --git a/src/doc/docs/examples/governator.md b/dropwizard-guicey/src/doc/docs/examples/governator.md similarity index 99% rename from src/doc/docs/examples/governator.md rename to dropwizard-guicey/src/doc/docs/examples/governator.md index 1b652daf3..9114fbd5f 100644 --- a/src/doc/docs/examples/governator.md +++ b/dropwizard-guicey/src/doc/docs/examples/governator.md @@ -56,7 +56,7 @@ to control governator lifecycle: import io.dropwizard.lifecycle.Managed; import ru.vyarus.dropwizard.guice.GuiceBundle; import com.netflix.governator.lifecycle.LifecycleManager; -import javax.inject.Inject; +import jakarta.inject.Inject; public class GovernatorLifecycle implements Managed { diff --git a/src/doc/docs/examples/hibernate.md b/dropwizard-guicey/src/doc/docs/examples/hibernate.md similarity index 92% rename from src/doc/docs/examples/hibernate.md rename to dropwizard-guicey/src/doc/docs/examples/hibernate.md index 2009ad4a6..6bf8dd934 100644 --- a/src/doc/docs/examples/hibernate.md +++ b/dropwizard-guicey/src/doc/docs/examples/hibernate.md @@ -1,17 +1,17 @@ # Hibernate integration -Example of [dropwizard-hibernate](https://www.dropwizard.io/en/release-2.0.x/manual/hibernate.html) bundle usage with guicey. +Example of [dropwizard-hibernate](https://www.dropwizard.io/en/release-4.0.x/manual/hibernate.html) bundle usage with guicey. !!! abstract "" - Example [source code](https://github.com/xvik/dropwizard-guicey-examples/tree/master/integration-hibernate) + Example [source code](https://github.com/xvik/dropwizard-guicey/examples/tree/master/integration-hibernate) ## Configuration Additional dependencies required: ```groovy - implementation 'io.dropwizard:dropwizard-hibernate:2.0.2' - implementation 'com.h2database:h2:1.4.199' + implementation 'io.dropwizard:dropwizard-hibernate:4.0.2' + implementation 'com.h2database:h2:2.2.224' ``` !!! note @@ -19,7 +19,7 @@ Additional dependencies required: For simplicity, an embedded H2 database is used. -Overall configuration is exactly the same as described in [dropwizard docs](https://www.dropwizard.io/en/release-2.0.x/manual/hibernate.html), +Overall configuration is exactly the same as described in [dropwizard docs](https://www.dropwizard.io/en/release-4.0.x/manual/hibernate.html), but extracted to separate class for simplicity: ```java diff --git a/src/doc/docs/examples/jdbi3.md b/dropwizard-guicey/src/doc/docs/examples/jdbi3.md similarity index 94% rename from src/doc/docs/examples/jdbi3.md rename to dropwizard-guicey/src/doc/docs/examples/jdbi3.md index aa52f5de8..2513880fe 100644 --- a/src/doc/docs/examples/jdbi3.md +++ b/dropwizard-guicey/src/doc/docs/examples/jdbi3.md @@ -3,7 +3,7 @@ Example of [guicey-jdbi3](../extras/jdbi3.md) extension usage. !!! abstract "" - Example [source code](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-jdbi3) + Example [source code](https://github.com/xvik/dropwizard-guicey/examples/tree/master/ext-jdbi3) The [JDBI3 extension](../extras/jdbi3.md) allows: @@ -19,14 +19,14 @@ The [JDBI3 extension](../extras/jdbi3.md) allows: Additional dependencies required: ```groovy -implementation 'ru.vyarus.guicey:guicey-jdbi3:{{ gradle.ext }}' -implementation 'com.h2database:h2:1.4.199' +implementation 'ru.vyarus.guicey:guicey-jdbi3:{{ gradle.version }}' +implementation 'com.h2database:h2:2.2.224' ``` !!! note Both versions are managed by [BOM](../extras/bom.md) -[dropwizard-jdbi3](https://www.dropwizard.io/en/release-2.0.x/manual/jdbi3.html) is used to configure +[dropwizard-jdbi3](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) is used to configure and create dbi instance: ```java @@ -66,10 +66,10 @@ database: !!! warning Database scheme must be created manually. You can use [dropwizard-flyway](https://github.com/dropwizard/dropwizard-flyway) module to prepare database. - See [example app source](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-jdbi3) for details. + See [example app source](https://github.com/xvik/dropwizard-guicey/examples/tree/master/ext-jdbi3) for details. -JDBI instance created exactly as described in [dropwizard docs](https://www.dropwizard.io/en/release-2.0.x/manual/jdbi3.html) +JDBI instance created exactly as described in [dropwizard docs](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) using provided db configuration: ```java @@ -261,7 +261,7 @@ public class UserResource { `UserMapper` and `UserBind` are used implicitly to convert the POJO into a db record and back. You can use `@InTransaction` on repository method to enlarge transaction scope, but, in contrast -to hibernate you dont't have to always declare it to avoid lazy initialization exception +to hibernate you don't have to always declare it to avoid lazy initialization exception (because jdbi produces simple pojos). !!! note diff --git a/src/doc/docs/extras/admin-rest.md b/dropwizard-guicey/src/doc/docs/extras/admin-rest.md similarity index 74% rename from src/doc/docs/extras/admin-rest.md rename to dropwizard-guicey/src/doc/docs/extras/admin-rest.md index da2662816..72933d862 100644 --- a/src/doc/docs/extras/admin-rest.md +++ b/dropwizard-guicey/src/doc/docs/extras/admin-rest.md @@ -1,8 +1,5 @@ # Admin REST -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-admin-rest) module - Mirror all resources in admin context: on admin side special servlet simply redirects all incoming requests into the jersey context. Hides admin-only resources from user context: resource is working under admin context and return 404 on user context. @@ -15,27 +12,23 @@ Features: ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-admin-rest.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-admin-rest) - -Remove `version` in dependency declaration below if you using [the BOM extensions](bom.md). - Maven: ```xml ru.vyarus.guicey guicey-admin-rest - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:guicey-admin-rest:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-admin-rest:{{ gradle.version }}' ``` -See the most recent version in the badge above. +Omit version if guicey BOM used ## Usage diff --git a/src/doc/docs/extras/bom.md b/dropwizard-guicey/src/doc/docs/extras/bom.md similarity index 62% rename from src/doc/docs/extras/bom.md rename to dropwizard-guicey/src/doc/docs/extras/bom.md index dd4988aa0..0534c9435 100644 --- a/src/doc/docs/extras/bom.md +++ b/dropwizard-guicey/src/doc/docs/extras/bom.md @@ -1,40 +1,14 @@ # Guicey BOM -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-bom) module - Maven BOM contains guicey and guicey ext modules versions. Also includes dropwizard and guice boms. !!! tip - BOM's are useful for versions management. After including bom you can simply include required dependencies + BOMs are useful for versions management. After including bom you can simply include required dependencies (dropwizard, guice, guicey, guicey-ext) without versions: bom will control all versions. -| BOM version | Guicey | Dropwizard | Guice | -|-------------|--------|------------|-------| -| 5.6.0-1 | 5.6.0 | 2.1.0 | 5.1.0 | -| 5.5.0-1 | 5.5.0 | 2.0.28 | 5.1.0 | -| 5.4.2-1 | 5.4.2 | 2.0.28 | 5.1.0 | -| 5.4.0-1 | 5.4.0 | 2.0.25 | 5.0.1 | -| 5.3.0-1 | 5.3.0 | 2.0.20 | 5.0.1 | -| 5.2.0-1 | 5.2.0 | 2.0.16 | 4.2.3 | -| 5.1.0-2 | 5.1.0 | 2.0.10 | 4.2.3 | -| 5.0.1-1 | 5.0.1 | 2.0.2 | 4.2.2 | -| 5.0.0-0 | 5.0.0 | 2.0.0 | 4.2.2 | -| 0.7.0 | 4.2.2 | 1.3.7 | 4.2.2 | -| 0.6.0 | 4.2.2 | 1.3.7 | 4.2.2 | -| 0.5.0 | 4.2.1 | 1.3.5 | 4.2.0 | -| 0.4.0 | 4.2.0 | 1.3.5 | 4.2.0 | -| 0.3.0 | 4.1.0 | 1.1.0 | 4.1.0 | - -Since 5.0.0 extension modules version is derived from guicey version: guiceyVersion-Number -(the same convention as for dropwizard modules). For example version 5.0.0-1 means -first extensions release (1) for guicey 5.0.0. ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-bom.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-bom) - - Maven: ```xml @@ -44,7 +18,7 @@ Maven: ru.vyarus.guicey guicey-bom - {{ gradle.ext }} + {{ gradle.version }} pom import @@ -83,7 +57,7 @@ Gradle: ```groovy // declare guice and ext modules without versions dependencies { - implementation platform('ru.vyarus.guicey:guicey-bom:{{ gradle.ext }}') + implementation platform('ru.vyarus.guicey:guicey-bom:{{ gradle.version }}') // uncomment to override dropwizard and its dependencies versions //implementation platform('io.dropwizard:dropwizard-dependencies:{{ gradle.dropwizard }}') @@ -93,12 +67,21 @@ dependencies { // Example of extension module usage implementation 'ru.vyarus.guicey:guicey-eventbus' } - ``` +Bom includes: + +BOM | Artifact +--------------|------------------------- +Guicey modules | `ru.vyarus.guicey:guicey-[module]` +Dropwizard BOM | `io.dropwizard:dropwizard-bom` +Guice BOM | `com.google.inject:guice-bom` +HK2 bridge | `org.glassfish.hk2:guice-bridge` +Spock-junit5 | `ru.vyarus:spock-junit5` + ## Dependencies override -You may override BOM version for any dependency by simply specifying exact version in dependecy declaration section. +You may override BOM version for any dependency by simply specifying exact version in dependency declaration section. If you want to use newer version (then provided by guicey BOM) of dropwizard or guice then import also their BOMs directly: diff --git a/src/doc/docs/extras/eventbus.md b/dropwizard-guicey/src/doc/docs/extras/eventbus.md similarity index 86% rename from src/doc/docs/extras/eventbus.md rename to dropwizard-guicey/src/doc/docs/extras/eventbus.md index fd693ccb7..7e988a673 100644 --- a/src/doc/docs/extras/eventbus.md +++ b/dropwizard-guicey/src/doc/docs/extras/eventbus.md @@ -1,9 +1,5 @@ # Guava EventBus integration -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-eventbus) module - - Integrates [Guava EventBus](https://github.com/google/guava/wiki/EventBusExplained) with guice. Features: @@ -14,27 +10,23 @@ Features: ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-eventbus.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-eventbus) - -Remove `version` in dependency declaration below if you using [the BOM extensions](bom.md). - Maven: ```xml ru.vyarus.guicey guicey-eventbus - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:guicey-eventbus:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-eventbus:{{ gradle.version }}' ``` -See the most recent version in the badge above. +Omit version if guicey BOM used ## Usage @@ -91,7 +83,7 @@ INFO [2016-12-01 12:31:02,819] ru.vyarus.guicey.eventbus.report.EventsReporter: !!! note Only subscriptions of beans registered at the time of injector startup will be shown. - For example, if MyBean has a subscription method but a binding for it is not declared (and noone depends on it), + For example, if MyBean has a subscription method but a binding for it is not declared (and no-one depends on it), a JIT binding will be created later in time (when bean will be actually used) and will not be reflected in the logs. ### Consuming multiple events diff --git a/src/doc/docs/extras/gsp.md b/dropwizard-guicey/src/doc/docs/extras/gsp.md similarity index 95% rename from src/doc/docs/extras/gsp.md rename to dropwizard-guicey/src/doc/docs/extras/gsp.md index 19b247952..d4a2c9691 100644 --- a/src/doc/docs/extras/gsp.md +++ b/dropwizard-guicey/src/doc/docs/extras/gsp.md @@ -1,8 +1,5 @@ # Guicey Server Pages -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-server-pages) module - Brings the simplicity of JSP to dropwizard-views. Basement for pluggable and extendable ui applications (like dashboards). @@ -11,7 +8,7 @@ Basement for pluggable and extendable ui applications (like dashboards). Features: -* Use standard dropwizard modules: [dropwizard-views](https://www.dropwizard.io/en/release-2.0.x/manual/views.html) and [dropwizard-assets](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#serving-assets) +* Use standard dropwizard modules: [dropwizard-views](https://www.dropwizard.io/en/release-4.0.x/manual/views.html) and [dropwizard-assets](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#serving-assets) * Support direct templates rendering (without rest resource declaration) * Static resources, direct templates and dropwizard-views rest endpoints are handled under the same url (like everything is stored in the same directory - easy to link css, js and other resources) @@ -123,32 +120,28 @@ Under the hood `/foo/12` will be recognized as template call and redirected (ser As you can see rest endpoints and templates are now "a part" of static resources.. just like good-old JSP (powered with rest mappings). And it is still pure dropwizard views. -GSP implements per-application error pages support so each application could use it's own errors. In pure -dropwizard-views such things should be implemented manually, which is not good for application incapsulation. +GSP implements per-application error pages support so each application could use its own errors. In pure +dropwizard-views such things should be implemented manually, which is not good for application encapsulation. ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-server-pages.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-gsp) - -Avoid version in dependency declaration below if you use [extensions BOM](../guicey-bom). - Maven: ```xml ru.vyarus.guicey guicey-server-pages - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -compile 'ru.vyarus.guicey:guicey-server-pages:{{ gradle.ext }}' +compile 'ru.vyarus.guicey:guicey-server-pages:{{ gradle.version }}' ``` -See the most recent version in the badge above. +Omit version if guicey BOM used ## Usage @@ -166,7 +159,7 @@ GuiceBundle.builder() ### Template engines -Out of the box [dropwizard provides](https://www.dropwizard.io/en/release-2.0.x/manual/views.html) `freemarker` and `mustache` engines support. +Out of the box [dropwizard provides](https://www.dropwizard.io/en/release-4.0.x/manual/views.html) `freemarker` and `mustache` engines support. You will need to add dependency to one of them (or both) in order to activate it (or, maybe, some third party engine): * implementation (`io.dropwizard:dropwizard-views-freemarker`) @@ -347,7 +340,7 @@ relatively to application mapping root ("/" in the example above) as `/page1/act By default, if views mapping is not declared manually, it would be set to application name (`/...` -> `/projectName-ui/...`) -Under startup dropwizard logs all registered rest enpoints, so you can always see original +Under startup dropwizard logs all registered rest endpoints, so you can always see original rest mapping paths. For each registered GSP application list of "visible" paths will be logged as: ``` @@ -381,7 +374,7 @@ But that's not all: you can actually map other rest prefixed to sub urls: ``` This way, it is possible to combine rest endpoints, written for different applications -(or simply prerare common view resource groups). Just note that in contrast to resources +(or simply prepare common view resource groups). Just note that in contrast to resources mapping, only one prefix may be mapped on each url! You will also need to map static resources location accordingly if you use relative template paths. @@ -468,7 +461,7 @@ If we call new page with `http://localhost:8080/sample/fred` we should see `@Template` annotation must be used on ALL template resources. It may declare default template for all methods in resource (`@Template("sample.ftl")`) or be just a marker annotation (`@Template`). -Annotation differentiate template resources from other api resources and lets you delare jersey +Annotation differentiate template resources from other api resources and lets you declare jersey extension only for template resources: ```java @@ -636,8 +629,8 @@ if (ex instanceof TracelessException) { for direct non 200 response code in rest. !!! important - GSP errors handling override [ExceptionMapper](https://www.dropwizard.io/en/release-2.0.x/manual/views.html#template-errors) - and [views errors](https://www.dropwizard.io/en/release-2.0.x/manual/views.html#custom-error-pages) + GSP errors handling override [ExceptionMapper](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#template-errors) + and [views errors](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#custom-error-pages) mechanisms because it intercept exceptions before them (using `RequestEventListener`)! So your `ExceptionMapper` will be called, but user will still see GSP error page. @@ -674,7 +667,7 @@ public class ErrorPage { ### SPA routing If you use Single Page Applications then you may face the need to recognize html5 client routing urls -and redirect to index page. You can read more about it in [guicey SPA module](../guicey-spa). +and redirect to index page. You can read more about it in [guicey SPA module](spa.md). As guicey SPA module can't be used directly with GSP, it's abilities is integrated directly and could be activated with: @@ -789,8 +782,8 @@ You can also map addition rest prefixes: ``` In some cases, extensions may depend on dropwizard configuration, but -bundles created under initialization phase. To workaround this you can -use delayed extensions init: +bundles created under initialization phase. To work around this you can +use delayed extension init: ```java .bundles(ServerPagesBundle.extendApp("projectName-ui") @@ -867,7 +860,7 @@ The same for admin app and extension. resources because it is not aware of custom loaders. !!! info - The main problem here is dropwizards `View` class which accepts only file path (String), + The main problem here is dropwizard's `View` class which accepts only file path (String), so even if correct URL object is known (which is enough to load resource) before view construction it can't be used further. @@ -885,4 +878,3 @@ ServerPagesBundle.builder() ``` For mustache module it is impossible to write such integration. - \ No newline at end of file diff --git a/src/doc/docs/extras/jdbi3.md b/dropwizard-guicey/src/doc/docs/extras/jdbi3.md similarity index 86% rename from src/doc/docs/extras/jdbi3.md rename to dropwizard-guicey/src/doc/docs/extras/jdbi3.md index 16afdcbbe..62f150035 100644 --- a/src/doc/docs/extras/jdbi3.md +++ b/dropwizard-guicey/src/doc/docs/extras/jdbi3.md @@ -1,9 +1,6 @@ # JDBI3 integration -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi3) module - -Integrates [JDBI3](http://jdbi.org/) with guice. Based on [dropwizard-jdbi3](https://www.dropwizard.io/en/release-2.0.x/manual/jdbi3.html) integration. +Integrates [JDBI3](http://jdbi.org/) with guice. Based on [dropwizard-jdbi3](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) integration. Features: @@ -18,39 +15,28 @@ Features: Added installers: -* [RepositoryInstaller](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java) - sql proxies -* [MapperInstaller](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java) - row mappers +* [RepositoryInstaller](https://github.com/xvik/dropwizard-guicey/blob/master/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java) - sql proxies +* [MapperInstaller](https://github.com/xvik/dropwizard-guicey/blob/master/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java) - row mappers ## Setup -!!! important - Since dropwizard 2.0.22 dropwizard-jdbi3 [requires Java 11 by default](https://github.com/dropwizard/dropwizard/releases/tag/v2.0.22), - use `guicey-jdbi3-jdk8` instead (meta package fixing classpath) for java 8 compatibility. - -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-jdbi3.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-jdbi3) - -Avoid version in dependency declaration below if you use [extensions BOM](../guicey-bom). - Maven: ```xml ru.vyarus.guicey guicey-jdbi3 - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:guicey-jdbi3:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-jdbi3:{{ gradle.version }}' ``` -See the most recent version in the badge above. - -!!! note "" - [Migration from jdbi2](jdbi.md#migration-to-jdbi3) +Omit version if guicey BOM used ## Usage @@ -63,7 +49,7 @@ GuiceBundle.builder() ``` Here default JDBI instance will be created from database configuration (much like it's described in -[dropwizard documentation](https://www.dropwizard.io/en/release-2.0.x/manual/jdbi3.html)). +[dropwizard documentation](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html)). Or build JDBI instance yourself: @@ -98,7 +84,7 @@ multiple objects in one transaction, you have to always create them manually for Integration removes these restrictions: dao (repository) objects are normal guice beans and transaction scope is controlled by `@InTransaction` annotation (note that such name was intentional to avoid confusion with -JDBI own's Transaction annotation and more common Transactional annotations). +JDBI's own Transaction annotation and more common Transactional annotations). At the beginning of unit of work, JDBI handle is created and bound to thread (thread local). All repositories are simply using this bound handle and so share transaction inside unit of work. @@ -138,8 +124,8 @@ Transaction isolation level and readonly flag could be defined with annotation: In case of nested transactions error will be thrown if: -* Current transaction level is different then nested one -* Current transaction is read only and nexted one is not (note that some drivers, like h2, ignore readOnly flag completely) +* Current transaction level is different than the nested one +* Current transaction is read only and nested transaction is not (note that some drivers, like h2, ignore readOnly flag completely) For example: @@ -240,7 +226,7 @@ public interface MyRepository { ``` Note the use of `@InTransaction`: it was used to be able to call repository methods without extra annotations -(the lowest transaction scope it's repository itself). It will make beans "feel the same" as usual JDBI on demand +(the lowest transaction scope its repository itself). It will make beans "feel the same" as usual JDBI on demand sql object proxies. `@InTransaction` annotation is handled using guice aop. You can use any other guice aop related features. @@ -291,7 +277,7 @@ In the eager mode all proxies would be constructed after application initializat ### Guice beans access -You can access guice beans by annotating getter with `@Inject` (javax or guice): +You can access guice beans by annotating getter with `@Inject` (jakarta or guice): ```java @JdbiRepository @@ -317,7 +303,7 @@ another proxy. ### Row mapper If you have custom implementations of `RowMapper`, it may be registered automatically. -You will be able to use injections there because mappers become ususal guice beans (singletons). +You will be able to use injections there because mappers become usual guice beans (singletons). When classpath scan is enabled, such classes will be searched and installed automatically. ```java @@ -367,4 +353,4 @@ try { } ``` -Repositories could also be called inside such manual unit (as unit of work is correctly started). \ No newline at end of file +Repositories could also be called inside such manual unit (as unit of work is correctly started). diff --git a/src/doc/docs/extras/lifecycle-annotations.md b/dropwizard-guicey/src/doc/docs/extras/lifecycle-annotations.md similarity index 81% rename from src/doc/docs/extras/lifecycle-annotations.md rename to dropwizard-guicey/src/doc/docs/extras/lifecycle-annotations.md index 33b2d8b15..29c4b372e 100644 --- a/src/doc/docs/extras/lifecycle-annotations.md +++ b/dropwizard-guicey/src/doc/docs/extras/lifecycle-annotations.md @@ -1,8 +1,5 @@ # Lifecycle annotations -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-lifecycle-annotations) module - Allows using lifecycle annotations for initialization/destruction methods in guice beans. Main motivation is to replace `Managed` usage in places where it's simpler to just annotate method, rather than register extension. @@ -13,28 +10,23 @@ register extension. ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-lifecycle-annotations.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-lifecycle-annotations) - -Avoid version in dependency declaration below if you use [extensions BOM](../guicey-bom). - Maven: ```xml ru.vyarus.guicey guicey-lifecycle-annotations - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:lifecycle-annotations:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:lifecycle-annotations:{{ gradle.version }}' ``` -See the most recent version in the badge above. - +Omit version if guicey BOM used ## Usage @@ -42,8 +34,8 @@ By default, no setup required: bundle will be loaded automatically with the bund So just add jar into classpath and annotations will work. ```java -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import ru.vyarus.guicey.annotations.lifecycle.PostStartup; public class SampleBean { @@ -67,7 +59,7 @@ public class SampleBean { * Annotated methods must not contain parameters. Method could have any visibility. * `@PostConstruct` or `@PostStartup` methods fail fails entire application startup (fail fast) -* `@PreDestroy` method fails are just logged to guarantee that all destroy methods will be procesed +* `@PreDestroy` method fails are just logged to guarantee that all destroy methods will be processed * If both current class and super class have annotated methods - both methods will be executed (the only obvious exception is overridden methods) !!! important @@ -101,4 +93,4 @@ new LifecycleAnnotationsBundle(new AbstractMatcher>() { return o.getRawType() != SomeExcludedBean.class; } }) -``` \ No newline at end of file +``` diff --git a/src/doc/docs/extras/spa.md b/dropwizard-guicey/src/doc/docs/extras/spa.md similarity index 87% rename from src/doc/docs/extras/spa.md rename to dropwizard-guicey/src/doc/docs/extras/spa.md index 961deb015..673ba5ebd 100644 --- a/src/doc/docs/extras/spa.md +++ b/dropwizard-guicey/src/doc/docs/extras/spa.md @@ -1,9 +1,6 @@ # Single page applications support -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-spa) module - -Provides a replacement for [dropwizard-assets](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#serving-assets) +Provides a replacement for [dropwizard-assets](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#serving-assets) bundle for single page applications (SPA) to properly handle html5 client routing. @@ -45,28 +42,23 @@ From example above, `/app/someroute` will return index page and `/app/css/some.c ## Setup - -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-spa.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-spa) - -Avoid version in dependency declaration below if you use [extensions BOM](../guicey-bom). - Maven: ```xml ru.vyarus.guicey guicey-spa - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:guicey-spa:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-spa:{{ gradle.version }}' ``` -See the most recent version in the badge above. +Omit version if guicey BOM used ## Usage @@ -94,7 +86,7 @@ Example registration to admin context: .bundles(SpaBundle.adminApp("admin", "/com/mycompany/adminapp/", "/manager").build()); ``` -Register "admin" application with resources in "/com/mycompany/adminapp/" package, served from "manager' +Register "admin" application with resources in "/com/mycompany/adminapp/" package, served from "manager" admin context (note that admin root is already used by dropwizard admin servlet). !!! tip @@ -143,4 +135,4 @@ Could be changed with: This regexp implements naive assumption that all app routes does not contain "extension". -Note: regexp is applied with `find` so use `^` or `$` to apply boundaries. \ No newline at end of file +Note: regexp is applied with `find` so use `^` or `$` to apply boundaries. diff --git a/src/doc/docs/extras/validation.md b/dropwizard-guicey/src/doc/docs/extras/validation.md similarity index 81% rename from src/doc/docs/extras/validation.md rename to dropwizard-guicey/src/doc/docs/extras/validation.md index 87a4315a7..d0bd42b59 100644 --- a/src/doc/docs/extras/validation.md +++ b/dropwizard-guicey/src/doc/docs/extras/validation.md @@ -1,9 +1,5 @@ # Validation -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-validation) module - - By default, dropwizard allows you to use validation annotations on [rest services](https://www.dropwizard.io/en/stable/manual/validation.html). This module allows you to use validation annotations the same way on any guice bean method. @@ -11,28 +7,23 @@ Bundle is actually a wrapper for [guice-validator](https://github.com/xvik/guice ## Setup -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-validation.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-validation) - -Avoid version in dependency declaration below if you use [extensions BOM](../guicey-bom). - Maven: ```xml ru.vyarus.guicey guicey-validation - {{ gradle.ext }} + {{ gradle.version }} ``` Gradle: ```groovy -implementation 'ru.vyarus.guicey:guicey-validation:{{ gradle.ext }}' +implementation 'ru.vyarus.guicey:guicey-validation:{{ gradle.version }}' ``` -See the most recent version in the badge above. - +Omit version if guicey BOM used ## Usage @@ -42,8 +33,8 @@ So just add jar into classpath and annotations will work. For example: ```java -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import ru.vyarus.guicey.annotations.lifecycle.PostStartup; public class SampleBean { @@ -107,7 +98,7 @@ Or excluding methods: ``` Now methods annotated with `@SuppressValidation` will not be validated. Note that -`.and(new DirectMethodMatcher())` condition was added to aslo exclude synthetic and bridge methods (jvm generated methods). +`.and(new DirectMethodMatcher())` condition was added to also exclude synthetic and bridge methods (jvm generated methods). !!! note You can verify AOP appliance with guicey `.printGuiceAopMap()` report. @@ -123,4 +114,4 @@ This could be disabled with bundle option: .bundles(new ValidationBundle().strictGroupsDeclaration()) ``` -Read more in [guice-validator docs](https://github.com/xvik/guice-validator#default-group-specifics). \ No newline at end of file +Read more in [guice-validator docs](https://github.com/xvik/guice-validator#default-group-specifics). diff --git a/src/doc/docs/getting-started.md b/dropwizard-guicey/src/doc/docs/getting-started.md similarity index 82% rename from src/doc/docs/getting-started.md rename to dropwizard-guicey/src/doc/docs/getting-started.md index 655a93d56..5351a471c 100644 --- a/src/doc/docs/getting-started.md +++ b/dropwizard-guicey/src/doc/docs/getting-started.md @@ -6,6 +6,8 @@ ## Installation +Core guicey could be used directly: + Maven: ```xml @@ -24,11 +26,8 @@ implementation 'ru.vyarus:dropwizard-guicey:{{ gradle.version }}' ### BOM -Guicey pom may be also used as maven BOM. - -!!! note - If you use guicey [extensions](guide/modules.md) then use [extensions BOM](extras/bom.md) - instead (it already includes guicey BOM). +But, it would be simpler to use it with BOM because of [simplified versions management for guice, dropwizard and guicey +modules](extras/bom.md): Gradle: @@ -40,13 +39,12 @@ dependencies { // no need to specify versions implementation 'ru.vyarus:dropwizard-guicey' - + + // example modules without versions implementation 'io.dropwizard:dropwizard-auth' implementation 'com.google.inject:guice-assistedinject' - testImplementation 'ru.vyarus:spock-junit5' - testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0' - testImplementation 'io.dropwizard:dropwizard-test' + testImplementation 'io.dropwizard:dropwizard-testing' } ``` @@ -81,20 +79,14 @@ Maven: ``` -BOM includes: +### Sample application -BOM | Artifact ---------------|------------------------- -Guicey itself | `ru.vyarus:dropwizard-guicey` -Dropwizard BOM | `io.dropwizard:dropwizard-bom` -Guice BOM | `com.google.inject:guice-bom` -HK2 bridge | `org.glassfish.hk2:guice-bridge` -Spock-junit5 | `ru.vyarus:spock-junit5` +There is also a [sample gradle application](https://github.com/xvik/dropwizard-app-todo) which could be used for a new project bootstrap. ## Usage !!! note "" - Full source of example application is [published here](https://github.com/xvik/dropwizard-guicey-examples/tree/master/core-getting-started) + Full source of example application is [published here](https://github.com/xvik/dropwizard-guicey/tree/master/examples/core-getting-started) Register guice bundle: @@ -108,7 +100,7 @@ public class SampleApplication extends Application { @Override public void initialize(Bootstrap bootstrap) { bootstrap.addBundle(GuiceBundle.builder() - .enableAutoConfig(getClass().getPackage().getName()) + .enableAutoConfig() .build()); } @@ -126,13 +118,26 @@ public class SampleApplication extends Application { the application package and subpackages. Extension classes are detected by "feature markers": for example, resources has `@Path` annotation, tasks extends `Task` etc. - !!! tip - You can declare multiple packages for classpath scan: + By default, auto configuration enabled for application package, but + you can declare manually any packages for classpath scan: ```java .enableAutoConfig("com.mycompany.foo", "com.mycompany.bar") ``` + If required, analysed classes could be [filtered](guide/scan.md#filter-classes). + For example, you can configure a spring-like approach of recognizing only annotated classes: + ```java + .autoConfigFilter(ClassFilters.annotated(Component.class, Service.class)) + ``` + This way, only classes annotated with `@Component` or `@Service` would be recognized. + + But the better way to use filters is to add additional skip annoatations: + ```java + .autoConfigFilter(ClassFilters.ignoreAnnotated(Skip.class)) + ``` + (so extensions, annoatated with `@Skip`, would be ignored by classpath scan) + The application could be launched by running main class (assumes you will use an IDE run command): ```bash @@ -142,6 +147,27 @@ SampleApplication server !!! note a config.yml is not passed as a parameter because we don't need additional configuration yet +### Application run + +Guice injector is created before `Application#run` method, so you could already +use injector inside it. + +To simplify usage, you can apply injections directly into application class: + +```java +public class App extends Application { + + @Inject MyService service; + + ... + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + service.doSomething(); + } +} +``` + ### Adding a Resource Create a custom rest resource class: @@ -176,8 +202,8 @@ Call `http://localhost:8080/sample/` to make sure it works. rootPath: '/rest/*' ``` -Resource is a guice bean, so you can use guice injection inside it. To access request scoped objects like `javax.servlet.http. -HttpServletRequest`, `javax.servlet.http.HttpServletResponse`, `javax.ws.rs.core.UriInfo`, `org.glassfish.jersey.server. +Resource is a guice bean, so you can use guice injection inside it. To access request scoped objects like `jakarta.servlet.http. +HttpServletRequest`, `jakarta.servlet.http.HttpServletResponse`, `jakarta.ws.rs.core.UriInfo`, `org.glassfish.jersey.server. ContainerRequest`, etc, you must wrap the desired objects in a `Provider`: ```java @@ -208,7 +234,7 @@ The example resource now obtains the caller's remote ip address and returns it i ### Adding a Managed Object -[Dropwizard managed objects](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#managed-objects) are extremely useful for managing resources. +[Dropwizard managed objects](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#managed-objects) are extremely useful for managing resources. Create a simple managed implementation: @@ -305,7 +331,18 @@ Multiple modules could be registered at once: !!! note The above registration occurs in dropwizard initialization phase, when neither `Configuration` nor `Environment` objects are available. If you need either of them in a module, you may register a module in - [guicey bundle's](guide/bundles.md#guicey-bundles) `run` method or use [marker interfaces](guide/guice/module-autowiring.md). + [guicey bundle's](guide/bundles.md#guicey-bundles) `run` method, use [marker interfaces](guide/guice/module-autowiring.md) or delayed bundle callback: + + ```java + bootstrap.addBundle(GuiceBundle.builder() + ... + // same as GuiceyBundle.run (kind of shortcut) + .whenConfigurationReady(env -> { + env.modules(new ConfAwareModule(env.configuration().getSomething())) + }) + .modules(new SampleModule()) + .build()); + ``` ## Manual mode @@ -323,7 +360,7 @@ bootstrap.addBundle(GuiceBundle.builder() .build()); ``` -The only difference is the absence of `.enableAutoConfig(...)` and the explicit declaration of desired extensions. +The only difference is the absence of `.enableAutoConfig()` and the explicit declaration of desired extensions. !!! tip Explicit extension declaration could be used together with `enableAutoConfig` (classpath scan). For example, @@ -358,7 +395,7 @@ public class SampleModule extends AbstractModule { Guicey will recognize all three bindings and register extensions. The difference with classpath scanning or manual declaration is only that guicey will not declare default bindings for extensions -(by default, guicey creates untargetted bindings for all extensions: `bind(Extension.class)`). +(by default, guicey creates untargeted bindings for all extensions: `bind(Extension.class)`). !!! tip An extension may be found three ways: by classpath scan, explicit extension declaration on the GuiceBundle, and by @@ -417,7 +454,7 @@ The Guicey Bundle **lifecycle and methods are the same** as Dropwizard Bundles. ```java public class MyBundle implements GuiceyBundle { - default void initialize(GuiceyBootstrap bootstrap) { + default void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.dropwizardBundles(new MyDropeizardBundle()); } } diff --git a/src/doc/docs/guice.md b/dropwizard-guicey/src/doc/docs/guice.md similarity index 86% rename from src/doc/docs/guice.md rename to dropwizard-guicey/src/doc/docs/guice.md index 60880d5e5..46a85d1a7 100644 --- a/src/doc/docs/guice.md +++ b/dropwizard-guicey/src/doc/docs/guice.md @@ -29,22 +29,22 @@ Main objects: Bindings below are not immediately available as HK2 context [starts after guice](guide/lifecycle.md): -* `javax.ws.rs.core.Application` -* `javax.ws.rs.ext.Providers` +* `jakarta.ws.rs.core.Application` +* `jakarta.ws.rs.ext.Providers` * `org.glassfish.hk2.api.ServiceLocator` * `org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider` Request-scoped bindings: -* `javax.ws.rs.core.UriInfo` -* `javax.ws.rs.container.ResourceInfo` -* `javax.ws.rs.core.HttpHeaders` -* `javax.ws.rs.core.SecurityContext` -* `javax.ws.rs.core.Request` +* `jakarta.ws.rs.core.UriInfo` +* `jakarta.ws.rs.container.ResourceInfo` +* `jakarta.ws.rs.core.HttpHeaders` +* `jakarta.ws.rs.core.SecurityContext` +* `jakarta.ws.rs.core.Request` * `org.glassfish.jersey.server.ContainerRequest` * `org.glassfish.jersey.server.internal.process.AsyncContext` -* `javax.servlet.http.HttpServletRequest` -* `javax.servlet.http.HttpServletResponse` +* `jakarta.servlet.http.HttpServletRequest` +* `jakarta.servlet.http.HttpServletResponse` !!! important "" Request scoped objects must be used through provider: @@ -104,6 +104,29 @@ See complete description in [the user guide](http://xvik.github.io/dropwizard-gu bindings. It is executed *before* injector creation and so could be used for problems diagnosis. Bindings [may change](guide/guice/bindings.md#value-by-path) with configuration values changes (e.g. `server` section depends on server implementation used). +You can also annotate any configuration property (or getter) with qualifier annotation +and property value [would be bound with this qualifier directly](guide/yaml-values.md#qualified-bindings): + +```java +public class MyConfig extends Configuration { + + @Named("custom") + private String prop1; + + @CustomQualifier + private SubObj obj1 = new SubObj(); + + ... + +@Singleton +public class MyService { + + @Inject @Named("custom") String prop; + @Inject @CustomQualifier SubObj obj; +} +``` + + ## Extensions and AOP As it [was mentioned](concepts.md#extensions) guice knows about extensions either by @@ -168,12 +191,17 @@ or [disable modules analysis](guide/guice/module-analysis.md#disabling-analysis) !!! note Guice bindings override (`Modules.override()`), available through guicey api [modulesOverride()](guide/guice/override.md), - will also cause syntetic module (because overrides are applied before calling injector factory). + will also cause synthetic module (because overrides are applied before calling injector factory). But this supposed to be used for tests only (just to mention). - + +!!! tip + If you have problems with startup time, guicey provides special reports for investigations: + + * [startup repport](guide/diagnostic/startup-report.md) + * [guice provision report](guide/diagnostic/guice-provision-report.md) + ## AOP Not guicey-related, but still, as it's not always obvious how AOP is applied on beans use [AOP report](guide/diagnostic/aop-report.md) - it shows all affected beans and (more importantly) applied aop handlers order. - \ No newline at end of file diff --git a/src/doc/docs/guide/bundles.md b/dropwizard-guicey/src/doc/docs/guide/bundles.md similarity index 88% rename from src/doc/docs/guide/bundles.md rename to dropwizard-guicey/src/doc/docs/guide/bundles.md index c11475008..d59917237 100644 --- a/src/doc/docs/guide/bundles.md +++ b/dropwizard-guicey/src/doc/docs/guide/bundles.md @@ -21,11 +21,20 @@ public interface ConfiguredBundle { } public interface GuiceyBundle { - default void initialize(GuiceyBootstrap bootstrap) {} + default void initialize(GuiceyBootstrap bootstrap) throws Exception {} default void run(GuiceyEnvironment environment) throws Exception {} } ``` +!!! note + Dropwizard bundles `initialize` method does not throw exceptions: assumed + only runtime exceptions (which are not handled by dropwizard). + + Initially, guicey bundle init method also did not allow checked exceptions. + But, it appears that often it is more useful to allow checked exceptions in init method to + avoid clumsy exception handling (especially for quick prototyping) and so checked exceptions + support was added. Runtime exceptions are rethrown as is. + Guicey Bundles are an extension to dropwizard bundles (without restrictions), so it is extremely simple to switch from dropwizard bundles. @@ -39,7 +48,7 @@ Example Guicey bundle: public class MyFeatureBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap .installers(MyFeatureExtensionInstaller.class) // dropwizard bundle usage @@ -81,13 +90,10 @@ Even more examples are in [extensions modules](../extras/bom.md) See all [bundle configuration options](configuration.md#guicey-bundle) !!! note - Most configurations only appear during the initialization phase. This was done in order - to follow dropwizard conventions (all configuration during init and all initialization on run). + Most configurations appear during the initialization phase. -The only exception to this rule is the registration of guice modules. Bundles are allowed to register modules in both phases. -Guice modules often require direct configuration values. Without this exception, Guicey Bundle authors would be required to create -wrappers around [guicey-aware](guice/module-autowiring.md) modules for proper guice registrations. Dropwizard itself shares a -similar exception in that HK2 modules may only be registered during the run phase. +On run phase it is possible to register guice modules, often requiring direct configuration values. +Also, some extension registration may depend on configuration value. ## Bundle De-duplication @@ -127,8 +133,8 @@ in tests (e.g. [HK2 scope control bundle](hk2.md#hk2-scope-debug)) or to install Bundle lookup is equivalent to registering bundle directly using builder `bundles` method. !!! note - Bundles from lookup will always be registered after all manually registered bundles - so you can use [de-cuplication](deduplication.md) to accept manual instance and deny lookup. + Bundles from lookup will always be registered after all manually registered bundles, + so you can use [de-duplication](deduplication.md) to accept manual instance and deny lookup. By default, two lookup mechanisms active: [by property](#system-property-lookup) and [with service loader](#service-loader-lookup). @@ -141,7 +147,7 @@ INFO [2019-10-17 14:50:14,304] ru.vyarus.dropwizard.guice.bundle.DefaultBundleL ru.vyarus.dropwizard.guice.diagnostic.support.bundle.LookupBundle ``` -You can disable default lookups with: +You can disable default look-ups with: ```java bootstrap.addBundle(GuiceBundle.builder() @@ -228,7 +234,7 @@ bootstrap.addBundle(GuiceBundle.builder() .build() ``` -To override the list of default lookups: +To override the list of default look-ups: ```java bootstrap.addBundle(GuiceBundle.builder() @@ -254,7 +260,7 @@ and in bundles: ```java public class MyBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.dropwizardBundle(new MyDwBundle()); } } @@ -267,7 +273,7 @@ public class MyBundle implements GuiceyBundle { ```java public class XIntegratuionBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap .dropwizardBundle(new DropwizardXBundle()); .modules(new XBindingsModule()) @@ -282,7 +288,7 @@ When you register dropwizard bundles through guicey api: * Bundle (and all transitive bundles) appear in [report](diagnostic/configuration-report.md) * Bundle itself or any transitive bundle could be [disabled](disables.md#disable-dropwizard-bundles) -* [De-duplication mechanism](deduplication.md#dropwizard-bundles) will work for bundle and it's transitive bundles +* [De-duplication mechanism](deduplication.md#dropwizard-bundles) will work for a bundle, and its transitive bundles So, if you have a "common bundle" problem (when 2 bundles register some common bundle and so you can use these bundles together) it could be solved just by registering bundle through the guicey api with @@ -304,7 +310,7 @@ bootstrap object: ```java public class MyBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.bootstrap().addBundle(new MyDwBundle()); } } diff --git a/src/doc/docs/guide/commands.md b/dropwizard-guicey/src/doc/docs/guide/commands.md similarity index 96% rename from src/doc/docs/guide/commands.md rename to dropwizard-guicey/src/doc/docs/guide/commands.md index 49284ac39..f7b9e2f76 100644 --- a/src/doc/docs/guide/commands.md +++ b/dropwizard-guicey/src/doc/docs/guide/commands.md @@ -90,5 +90,5 @@ public class SyncCommand extends EnvironmentCommand { } ``` -This example shows workaround for managed initialization in commnads: `DbManager` is some `Managed` bean which would run automatically -in server mode. But commands never call managed objects, so we have to manually start and stop them. \ No newline at end of file +This example shows workaround for managed initialization in commands: `DbManager` is some `Managed` bean which would run automatically +in server mode. But commands never call managed objects, so we have to manually start and stop them. diff --git a/src/doc/docs/guide/configuration.md b/dropwizard-guicey/src/doc/docs/guide/configuration.md similarity index 82% rename from src/doc/docs/guide/configuration.md rename to dropwizard-guicey/src/doc/docs/guide/configuration.md index c6fc80679..ba99d9801 100644 --- a/src/doc/docs/guide/configuration.md +++ b/dropwizard-guicey/src/doc/docs/guide/configuration.md @@ -25,7 +25,13 @@ Guicey could be configured through: ### Configuration items `#!java .enableAutoConfig(String... basePackages)` -: Enable [classpath scan](scan.md) for automatic extensions registration, custom installers search and commands search (if enabled) +: Enable [classpath scan](scan.md) for automatic extension registration, custom installers search and commands search (if enabled) + +`#!java .enableAutoConfig()` +: (without packages) Enabling classpath scan on application package + +`#!java .autoConfigFilter(Predicate)` +: [Filter](scan.md#filter-classes) classes to scan `#!java .modules(Module... modules)` : Guice modules registration @@ -49,11 +55,11 @@ Guicey could be configured through: Custom installers are registered automatically when [classpath scan](scan.md) is enabled. `#!java .extensions(Class... extensionClasses)` -: Manual extensions registration. May be used together with [classpath scan](scan.md) and +: Manual extension registration. May be used together with [classpath scan](scan.md) and [binding extensions](guice/module-analysis.md#extensions-recognition) `#!java .extensionsOptional(Class... extensionClasses)` -: Optional extensions registration. The difference with `.extensions` is that such extensions +: Optional extension registration. The difference with `.extensions` is that such extensions will be automatically disabled if there are no compatible installers (instead of throwing exception). `#!java .bundles(GuiceyBundle... bundles)` @@ -62,7 +68,7 @@ Guicey could be configured through: `#!java .bundleLookup(GuiceyBundleLookup bundleLookup)` : Custom [lookup mechanism](bundles.md#bundle-lookup) for guicey bundles. By default, lookup [by system property](bundles.md#system-property-lookup) and [ServiceLoader](bundles.md#service-loader-lookup) - are enabled. To disable all lookups use: `#!java .disableBundleLookup()` + are enabled. To disable all look-ups use: `#!java .disableBundleLookup()` `#!java .dropwizardBundles(ConfiguredBundle... bundles)` : Shortcut for [dropwizard bundles](bundles.md#dropwizard-bundles) registration. This way guicey could apply @@ -95,7 +101,7 @@ Could be also useful to "hack" third party items. ### Items de-duplication -Guiey detects instances of the same type (bundles, modules). By default, two instances +Guicey detects instances of the same type (bundles, modules). By default, two instances considered as [duplicates](deduplication.md) if they are equal, so duplicates could be controlled with proper equals method implementation. When it's not possible, custom de-duplication [implementation](deduplication.md#general-unique-logic) could be used. @@ -138,7 +144,7 @@ BindConfigurationByPath | Boolean | true | [Introspect configuration](yaml-value TrackDropwizardBundles | Boolean | true | Recognize [transitive](bundles.md#transitive-bundles-tracking) dropwizard bundles (for bundles registered through guicey api) AnalyzeGuiceModules | Boolean | true | [Extension recognition](guice/module-analysis.md#extensions-recognition) in guice bindings, [transitive modules](guice/module-analysis.md#transitive-modules) disable support GuiceFilterRegistration | `EnumSet` | [REQUEST] | [Guice filter](guice/servletmodule.md) registration options -UseHkBridge | Boolean | false | Activates [HK2-guice bridge](hk2.md#hk2-guice-bridge) (bridge dependency must be avaiable in classpath) +UseHkBridge | Boolean | false | Activates [HK2-guice bridge](hk2.md#hk2-guice-bridge) (bridge dependency must be available on the classpath) #### InstallersOptions @@ -152,7 +158,7 @@ ForceSingletonForJerseyExtensions | Boolean | true | Force [singleton](../instal `#!java .injectorFactory(InjectorFactory injectorFactory)` : Use custom [injector factory](guice/injector.md#injector-factory) implementation. May be useful for tests or for [integration](test/overview.md#overriding-overridden-beans) - of 3rd paty library (like [governator](../examples/governator.md)) + of 3rd party library (like [governator](../examples/governator.md)) `#!java .build(Stage stage)` : Build bundle with custom [guice stage](guice/injector.md#injector-stage) (by default, `Production`) @@ -164,22 +170,43 @@ ForceSingletonForJerseyExtensions | Boolean | true | Force [singleton](../instal `#!java .listen(GuiceyLifecycleListener... listeners)` : Listen for guicey lifecycle [events](events.md#listeners) + +`#!java .onGuiceyStartup(GuiceyStartupListener listener)` +: Shortcut for manual configuration under run phase with available injector + +`#!java .onApplicationStartup(ApplicationStartupListener listener)` +: Shortcut for manual actions after complete application start (jetty started) + + !!! note "" + It is also called after guicey initialization in lightweight guicey tests + +`#!java .onApplicationShutdown(ApplicationShutdownListener listener)` +: Shortcut for manual actions after complete application shutdown + +`#!java .listenServer(ServerLifecycleListener listener)` +: Shortcut for `#!java environment().lifecycle().addServerLifecycleListener()` + +`#!java .listenJetty(LifeCycle.Listener listener)` +: Shortcut for `#!java environment().lifecycle().addLifeCycleListener()` + +`#!java .listenJersey(new ApplicationEventListener() {...})` +: Shortcut for `#!java environment.jersey().register(listener)` `#!java .noGuiceFilter()` -: Disable [GucieFilter](guice/servletmodule.md) registration. +: Disable [GuiceFilter](guice/servletmodule.md) registration. !!! danger "" This will [remove](guice/servletmodule.md#disable-servletmodule-support) guice request and session scopes and also it would become impossible to use `ServletModule`s `#!java .strictScopeControl()` -: Explicitly detect when gucie bean is instantiated with HK2 and vice versa. +: Explicitly detect when guice bean is instantiated with HK2 and vice versa. !!! note "" Bean target container is defined with `@JerseyManaged` and `@GuiceManaged` annotations or default (either guice or [hk2 used as default](hk2.md#use-hk2-for-jersey-extensions) (for jersey extensions)) `#!java .useHK2ForJerseyExtensions()` : Use [HK2 by default](hk2.md#use-hk2-for-jersey-extensions) for jersey extensions (change default). With this `@GuiceManaged` annotation - may be used to override defual for bean. + may be used to override default for bean. !!! danger "" Beans managed by HK2 can't use guice AOP, so AOP-based features will not work with such beans @@ -207,6 +234,20 @@ See [diagnostic section](diagnostic/diagnostic-tools.md) for a full list of avai : This method is mainly useful for hooks, because it's the only way to access application [shared state](shared.md) from [hook](#hooks). +### Configuration awareness + +To configure bundle when application configuration object is required: + +```java +GuiceBundle.builder() + ... + .whenConfigurationReady(env -> { + // env is GuiceyEnvironment, like in GuiceyBundle#run + AppConfig config = env.configuration(); + env.modules(new GuiceModule(config)); + }); +``` + ## Guicey bundle `GuiceyBundle`s are like dropwizard bundles, but with [greater abilities](bundles.md). Supposed to be used instead of @@ -219,7 +260,7 @@ dropwizard bundles. Bundles are registered either directly (in main bundle or ot public class MyBundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { ... } } @@ -276,14 +317,16 @@ Shortcuts: public class MyBundle implements GuiceyBundle { @Override - public void run(GuiceyEnvironment environment) { + public void run(GuiceyEnvironment environment) throws Exception { ... } } ``` -Everything is configured under initialization phase. On run phase -bundle allows only modules registration and extensions disable. +All bundles must be registered under initialization phase. + +On run phase modules and extensions registration is available (to perform registration, +using application configuration, or avoid extension registration based on configuration value). Shortcuts: @@ -305,11 +348,17 @@ Shortcuts: `#!java .listenJetty(LifeCycle.Listener listener)` : Shortcut for `#!java environment().lifecycle().addLifeCycleListener()` +`#!java .listenJersey(new ApplicationEventListener() {...})` +: Shortcut for `#!java environment.jersey().register(listener)` + + [Extended configuration](yaml-values.md) access: `#!java .configuration(String yamlPath)` `#!java .configuration(Class type)` `#!java .configurations(Class type)` +`#!java .annotatedConfiguration(ann)` +`#!java .annotatedConfiguration(Class)` `#!java .configurationTree()` Modules registration: @@ -350,6 +399,9 @@ Guicey [listeners](events.md#listeners): !!! note "" It is also called after guicey initialization in lightweight guicey tests +`#!java .onApplicationShutdown(ApplicationShutdownListener listener)` +: Shortcut for manual actions after complete application shutdown + ## Hooks Guicey [hooks](hooks.md) are registered statically **before** main guice bundle registration: @@ -371,4 +423,4 @@ On execution hook receives *the same* builder as used in main `GuiceBundle`. So hooks could configure *everything*. Hooks are intended to be used in tests and to implement a pluggable [diagnostic tools](hooks.md#diagnostic) -activated with system property `-Dguicey.hooks=...` (as an example, see guicey `DiagnosticHook`). \ No newline at end of file +activated with system property `-Dguicey.hooks=...` (as an example, see guicey `DiagnosticHook`). diff --git a/src/doc/docs/guide/deduplication.md b/dropwizard-guicey/src/doc/docs/guide/deduplication.md similarity index 95% rename from src/doc/docs/guide/deduplication.md rename to dropwizard-guicey/src/doc/docs/guide/deduplication.md index c36110216..46078d7df 100644 --- a/src/doc/docs/guide/deduplication.md +++ b/dropwizard-guicey/src/doc/docs/guide/deduplication.md @@ -20,14 +20,14 @@ install the same common bundle it would be installed twice: ```java public class Feature1Bundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.bundles(new CommonBundle); } } public class Feature2Bundle implements GuiceyBundle { @Override - public void initialize(GuiceyBootstrap bootstrap) { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { bootstrap.bundles(new CommonBundle); } } @@ -37,7 +37,7 @@ GuiceBundle.buider() ... ``` -To workaround such cases *deduplication mechanism** was introduced: instances of the same +To work around such cases *deduplication mechanism** was introduced: instances of the same type are considered duplicate if they are equal. ## Equals method @@ -204,7 +204,7 @@ public class MyModule extends UniqueModule {} Then guice will perform de-duplication itself. !!! warning - Guice will perform de-deplication itself only if both `equals` and `hashCode` properly implemented + Guice will perform de-duplication itself only if both `equals` and `hashCode` properly implemented (like in `UniqueModule`) @@ -251,4 +251,4 @@ bootstrap.addBundle(GuiceBindle.builder() ``` This **will not work** because guicey see only directly registered bundles: `OtherBundle` and transitive `MyBundle`, -and so `MyBundle` would be registered twice. \ No newline at end of file +and so `MyBundle` would be registered twice. diff --git a/src/doc/docs/guide/diagnostic/aop-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/aop-report.md similarity index 95% rename from src/doc/docs/guide/diagnostic/aop-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/aop-report.md index 7605fff2a..a2eaefe1b 100644 --- a/src/doc/docs/guide/diagnostic/aop-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/aop-report.md @@ -1,6 +1,6 @@ # AOP report -Guice AOP report shows all registered aop handlers and how (what order) they apply to gucie beans. +Guice AOP report shows all registered aop handlers and how (what order) they apply to guicey beans. ```java GuiceBundle.builder() @@ -90,7 +90,7 @@ All appliances of exact interceptor: ## Report customization -Report is implemented as guicey [event listener](../events.md) so you can register it directly +Report is implemented as a guicey [event listener](../events.md), so you can register it directly in your bundle if required (without main bundle shortcuts): ```java @@ -102,4 +102,4 @@ implementing `ReportRenderer`. Renderer not bound to guice context and assume di For examples of direct renderer usage see [events](../events.md) implementation: -* `InjectorPhaseEvent.ReportRenderer` \ No newline at end of file +* `InjectorPhaseEvent.ReportRenderer` diff --git a/src/doc/docs/guide/diagnostic/configuration-model.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-model.md similarity index 96% rename from src/doc/docs/guide/diagnostic/configuration-model.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-model.md index e58303b68..a40374a61 100644 --- a/src/doc/docs/guide/diagnostic/configuration-model.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-model.md @@ -101,7 +101,7 @@ All raw data is actually available through: `GuiceyConfigurationInfo#getData()`, but `GuiceyConfigurationInfo` provides many shortcut methods to simplify raw data querying. Data querying is based on java `Predicate` usage. `Filters` class provides common predicate builders. -Many examples of it's usage may be found in code. +Many examples of its usage may be found in code. !!! note For some reports it is important to know only types of used configuration items, @@ -126,7 +126,7 @@ More advanced queries are applied with predicate composition: } ``` -For exact configuration item type you can always get it's configuration model: +For exact configuration item type you can always get its configuration model: ```java @Inject GuiceyConfigurationInfo info; @@ -140,7 +140,7 @@ For instance-based items, you can receive all models for instances of type: List models = info.getInfos(MyBundle.class) ``` -And the last example is if you know exact extension instance and wasn't to get it's info: +And the last example is if you know exact extension instance and wasn't to get its info: ```java BundleItemInfo model = info.getData().getInfo(ItemId.from(myBundleInstance)) @@ -160,7 +160,7 @@ List modules = info.getModuleIds().stream() .collect(Collectors.toList()); ``` -Here all used module ids (`ItemId`) obtained. Then complete configuration model loaded for each item and +Here all used module ids (`ItemId`) obtained. Then complete configuration model loaded for each item and instance obtained from model. !!! note @@ -188,4 +188,4 @@ this option value was ever queried (by application). ## Configuration tree -[Parsed configuration](../yaml-values.md) object is available through `GuiceyConfigurationInfo#getConfigurationTree()` \ No newline at end of file +[Parsed configuration](../yaml-values.md) object is available through `GuiceyConfigurationInfo#getConfigurationTree()` diff --git a/src/doc/docs/guide/diagnostic/configuration-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-report.md similarity index 97% rename from src/doc/docs/guide/diagnostic/configuration-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-report.md index e76680a87..201b3c3a1 100644 --- a/src/doc/docs/guide/diagnostic/configuration-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/configuration-report.md @@ -203,9 +203,9 @@ INFO [2019-10-11 04:25:47,022] ru.vyarus.dropwizard.guice.debug.ConfigurationDi ``` Guicey time (`431.2 ms`) is measured as `GuiceBundle` methods plus part of jersey configuration time (jersey started after bundle). -It also show time spent on each application starting phase: `150.2 ms` configuration (initialization), `279.4 ms` run and `1.594 ms` during jersey startup. +It also shows time spent on each application starting phase: `150.2 ms` configuration (initialization), `279.4 ms` run and `1.594 ms` during jersey startup. -All items below represent guicey time detalization. Tree childs always detail time of direct parent. +All items below represent guicey time details. Each child item always includes the time detail of the direct parent. !!! tip Application startup during development may be improved with VM options: @@ -236,7 +236,7 @@ Guicey will later use this resolved classes to search commands (if enabled), ins !!! note Classpath scan time will be obviously bigger for real applications (with larger classes count). - But most of this time spent on class loading (becauase guicey loads all classes during scan and not + But most of this time spent on class loading (because guicey loads all classes during scan and not just parse class structure). If you use all these classes then they will be loaded in any case. If you disable classpath scan to save time then this time will just move to other places (where classes are used). @@ -417,8 +417,8 @@ Used markers: * `NOT_USED` - option was set by user but never used !!! note - `NOT_USED` marker just indicates that option is "not yet" used. Options may be consumed lazilly by application logic, so - it is possible that its not used at reporting time. There is no such cases with guicey options, + `NOT_USED` marker just indicates that option is "not yet" used. Options may be consumed lazily by application logic, so + it is possible that it's not used at reporting time. There is no such cases with guicey options, but may be with your custom options (it all depends on usage scenario). ## Configuration summary @@ -622,7 +622,7 @@ Extensions detected from guice bindings are shown as a sub tree: see configuration source. Bindings are not shown under main configuration tree (where modules are registered) because -guicey know only module class, but actually moduliple module instances could be registered +guicey only knows about the module class, but actually multiple module instances could be registered and so it is impossible to known what module instance extension is related to. !!! tip @@ -654,4 +654,4 @@ implementing `ReportRenderer`. Renderers not bound to guice context and assume d For examples of direct renderers usage see [events](../events.md) implementation: -* `InjectorPhaseEvent.ReportRenderer` \ No newline at end of file +* `InjectorPhaseEvent.ReportRenderer` diff --git a/src/doc/docs/guide/diagnostic/diagnostic-tools.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/diagnostic-tools.md similarity index 88% rename from src/doc/docs/guide/diagnostic/diagnostic-tools.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/diagnostic-tools.md index d440deda5..029eaf9d7 100644 --- a/src/doc/docs/guide/diagnostic/diagnostic-tools.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/diagnostic-tools.md @@ -3,6 +3,12 @@ Guicey provide many bundled console reports to help with problems diagnostic (or to simply clarify how application works) during development. All reports may be enabled on main guice bundle: +`#!java .printStartupTime()` +: Detailed application [startup time](startup-report.md) + +`#!java .printExtensionsHelp()` +: Extensions [recognition help](extensions-report.md) + `#!java .printDiagnosticInfo()` : Detailed [guicey configuration](configuration-report.md) information @@ -15,14 +21,17 @@ during development. All reports may be enabled on main guice bundle: `#!java .printGuiceBindings()` `#!java .printAllGuiceBindings()` -: [Guice bindings](guice-report.md) from registered modules +: [Guice bindings](guice-report.md) from registered modules + +`#!java .printGuiceProvisionTime()` +: [Guice provision time](guice-report.md) `#!java .printGuiceAopMap()` `#!java .printGuiceAopMap(GuiceAopConfig config)` : [AOP appliance](aop-report.md) map `#!java .printWebMappings()` -: Prints all registered [resvlets and filters](web-report.md) (including guice `ServletModule` declarations) +: Prints all registered [servlets and filters](web-report.md) (including guice `ServletModule` declarations) `#!java .printJerseyConfig()` : Prints all registered jersey extensions (exception mappers, filters etc.): including everything registered by @@ -32,6 +41,9 @@ during development. All reports may be enabled on main guice bundle: `#!java .printLifecyclePhasesDetailed()` : [Guicey lifecycle stages](lifecycle-report.md) (separates logs to clearly see what messages relates to what phase) +`#!java .printSharedStateUsage()` +: [Shared state usage](shared-state-report.md) + `#!java .strictScopeControl()` : In case of doubts about extension owner (guice or HK2) and suspicious for duplicate instantiation, you can enable [strict control](../hk2.md#hk2-scope-debug) which will throw exception in case of wrong owner. @@ -39,7 +51,7 @@ during development. All reports may be enabled on main guice bundle: ## Diagnostic hook It is obviously impossible to enable diagnostic reports without application re-compilation. -But, sometimes, it is required to validate installed application. To workaround this situation, +But, sometimes, it is required to validate installed application. To work around this situation, guicey provides special diagnostic hook, which can be enabled with a system property: ``` @@ -101,4 +113,3 @@ For examples of direct renderers usage see [events](../events.md) implementation !!! note These shortcut methods allow easy render of report into string using received event object (in listener). - \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/diagnostic/extensions-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/extensions-report.md new file mode 100644 index 000000000..94879d0c2 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/extensions-report.md @@ -0,0 +1,87 @@ +# Extensions help + +Shows extension signs recognized by registered installers. + +```java +GuiceBundle.builder() + ... + .printExtensionsHelp() + .build() +``` + +Example report: + +``` +INFO [2022-12-28 14:57:01,445] ru.vyarus.dropwizard.guice.debug.ExtensionsHelpDiagnostic: Recognized extension signs + + lifecycle (r.v.d.g.m.i.f.LifeCycleInstaller) + implements LifeCycle + + managed (r.v.d.g.m.i.feature.ManagedInstaller) + implements Managed + + jerseyfeature (r.v.d.g.m.i.f.j.JerseyFeatureInstaller) + implements Feature + + jerseyprovider (r.v.d.g.m.i.f.j.p.JerseyProviderInstaller) + @Provider on class + implements ExceptionMapper + implements ParamConverterProvider + implements ContextResolver + implements MessageBodyReader + implements MessageBodyWriter + implements ReaderInterceptor + implements WriterInterceptor + implements ContainerRequestFilter + implements ContainerResponseFilter + implements DynamicFeature + implements ValueParamProvider + implements InjectionResolver + implements ApplicationEventListener + implements ModelProcessor + + resource (r.v.d.g.m.i.f.j.ResourceInstaller) + @Path on class + @Path on implemented interface + + eagersingleton (r.v.d.g.m.i.f.e.EagerSingletonInstaller) + @EagerSingleton on class + + healthcheck (r.v.d.g.m.i.f.h.HealthCheckInstaller) + extends NamedHealthCheck + + task (r.v.d.g.m.i.feature.TaskInstaller) + extends Task + + plugin (r.v.d.g.m.i.f.plugin.PluginInstaller) + @Plugin on class + custom annotation on class, annotated with @Plugin + + webservlet (r.v.d.g.m.i.f.w.WebServletInstaller) + extends HttpServlet + @WebServlet + + webfilter (r.v.d.g.m.i.f.web.WebFilterInstaller) + implements Filter + @WebFilter + + weblistener (r.v.d.g.m.i.f.w.l.WebListenerInstaller) + implements EventListener + @WebListener +``` + +All signs are grouped by installer. Installers listed in processing order, which is important, +because first installer recognized extension "owns" it (even if extension contains signs, recognizable by other installers). + +## Custom installers + +Custom installers should implement new method to participate in report (not required!). +Example implementation from singleton installer: + +```java +public class EagerSingletonInstaller implements FeatureInstaller { + ... + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("@" + EagerSingleton.class.getSimpleName() + " on class"); + } +} +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-provision-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-provision-report.md new file mode 100644 index 000000000..bb791d994 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-provision-report.md @@ -0,0 +1,102 @@ +# Guice provision time + +The report intended to show the time of guice beans provision (instance construction, +including provider or provider method time). It shows all requested guice beans and the +number of obtained instances (for prototype scopes). + +```java +GuiceBundle.builder() + .printGuiceProvisionTime() +``` + +All provisions are sorted by time: + +``` +INFO [2025-03-27 09:20:32,313] ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic: Guice bindings provision time: + + Overall 57 provisions took 1.40 ms + binding [@Singleton] ManagedFilterPipeline : 0.88 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:94) + binding [@Singleton] ManagedServletPipeline : 0.45 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:95) + providerinstance [@Singleton] @ScopingOnly GuiceFilter : 0.02 ms com.google.inject.servlet.InternalServletModule.provideScopingOnlyGuiceFilter(InternalServletModule.java:106) + JIT [@Prototype] JitService x10 : 0.02 ms (0.006 ms + 0.002 ms + 0.001 ms + 0.001 ms + 0.001 ms + ...) + binding [@Singleton] GuiceyConfigurationInfo : 0.01 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:63) + binding [@Singleton] BackwardsCompatibleServletContextProvider : 0.007 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:99) + instance [@Singleton] Bootstrap : 0.004 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:71) + instance [@Singleton] @Config("server.gzip.minimumEntitySize") DataSize : 0.002 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:129) + instance [@Singleton] Environment : 0.0009 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:72) + instance [@Singleton] @Config AdminFactory : 0.0008 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117) + ... +``` + +The report will also try to detect injection mistakes in case when JIT (just in time) binding is used +when there are qualified declarations with the same type. + +The most common mistake is configuration objects misuse: guicey binds unique configuration objects +with `@Config` qualifier, but, if injection point declared without the qualifier, +guice will create a JIT binding (create new object instance) instead of injecting +declared instance. This might be hard to spot, especially when lombok is used (which may not +copy field annotation into constructor). + +``` +INFO [2025-03-27 09:21:33,438] ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic: Guice bindings provision time: + + Possible mistakes (unqualified JIT bindings): + + @Inject Sub: + instance [@Singleton] @Config("val2") Sub : 0.0005 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:129) + instance [@Singleton] @Marker Sub : 0.0007 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindCustomQualifiers(ConfigBindingModule.java:87) + > JIT [@Prototype] Sub : 0.006 ms + + @Inject Uniq: + instance [@Singleton] @Config Uniq : 0.0005 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117) + > JIT [@Prototype] Uniq : 0.004 ms + + Overall 53 provisions took 1.45 ms + binding [@Singleton] ManagedFilterPipeline : 0.78 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:94) + binding [@Singleton] ManagedServletPipeline : 0.44 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:95) +``` + +In this example, the report detects incorrect injections: + +```java + @Inject + private Sub val; + @Inject + private Uniq uniq; +``` + +Detection will also work for generified bindings: + +``` + Possible mistakes (unqualified JIT bindings): + + @Inject Service: + instance [@Singleton] Service : 0.0006 ms ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:46) + instance [@Singleton] Service : 0.002 ms ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:45) + > JIT [@Prototype] Service : 0.004 ms + +``` + +The report could be also enabled for compiled application: `-Dguicey.hooks=provision-time` + +The report shows only provisions performed on application startup, but it could be used in +tests to detect provision problems at runtime: + +```java + @EnableHook + static GuiceProvisionTimeHook report = new GuiceProvisionTimeHook(); + +@Test +void testRuntimeReport() { + // clear startup data + report.clearData(); + // do something that might cause additional provisions + injector.getInstance(Service.class); + injector.getInstance(Service.class); + + // assert + Assertions.assertThat(report.getRecordedData().keys().size()).isEqualTo(2); + // or just print report (only for recorded provisions) + System.out.println(report.renderReport()); +} +``` \ No newline at end of file diff --git a/src/doc/docs/guide/diagnostic/guice-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-report.md similarity index 99% rename from src/doc/docs/guide/diagnostic/guice-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-report.md index b276f0ba4..97f7c96f9 100644 --- a/src/doc/docs/guide/diagnostic/guice-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/guice-report.md @@ -102,7 +102,7 @@ instance | `#!java bing(Smth.class).toInstance(obj)` providerinstance | `#!java bind(Smth.class).toProvider(obj)` linkedkey | `#!java bind(Smth.class).to(Other.class)` (`Other` may be already declared with separate binding) providerkey | `#!java bind(Smth.class).toProvider(DmthProv.class)` -untargetted | `#!java bind(Smth.class)` +untargeted | `#!java bind(Smth.class)` providermethod | Module method annotated with `#!java @Provides` exposed | `#!java expose(PrivateService.class)` (service expose in `PrivateModule`) @@ -286,4 +286,4 @@ implementing `ReportRenderer`. Renderer not bound to guice context and assume di For examples of direct renderer usage see [events](../events.md) implementation: -* `InjectorPhaseEvent.ReportRenderer` \ No newline at end of file +* `InjectorPhaseEvent.ReportRenderer` diff --git a/src/doc/docs/guide/diagnostic/installers-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/installers-report.md similarity index 92% rename from src/doc/docs/guide/diagnostic/installers-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/installers-report.md index e2049c6e6..0de9f7613 100644 --- a/src/doc/docs/guide/diagnostic/installers-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/installers-report.md @@ -57,7 +57,7 @@ INFO [2019-10-11 06:09:06,085] ru.vyarus.dropwizard.guice.debug.ConfigurationDi !!! note This is actually re-configured [configuration report](configuration-report.md). - But, in contrast to configration report, it shows all installers (*even not used*). + But, in contrast to configuration report, it shows all installers (*even not used*). Also, it indicated used installer features. For example, looking at @@ -84,6 +84,6 @@ Feature | Description `OBJECT` | Installer use object instances for extensions registration (obtain instance from guice context) `TYPE` | Installer use extension class for extension registration. Usually it's jersey installers which has to register extension in jersey context `JERSEY` | Installer installs jersey features (in time of jersey start and not after injector creation as "pure" installers) -`BIND` | Installer perform manual guice binding. For such installers, automatic untargetted binding for extension is not created (assuming installer require some custom binding). Such installers also verify manual gucie bindings, when they are recognized as extension binding. +`BIND` | Installer perform manual guice binding. For such installers, automatic untargeted binding for extension is not created (assuming installer require some custom binding). Such installers also verify manual guice bindings, when they are recognized as extension binding. `OPTIONS` | Installer requires access for options. Most likely it means it supports additional configuration options (but it cold just read core options value). -`ORDER` | Installer supports extensions ordering. Use `@Order` annotation to declare order. \ No newline at end of file +`ORDER` | Installer supports extensions ordering. Use `@Order` annotation to declare order. diff --git a/src/doc/docs/guide/diagnostic/jersey-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/jersey-report.md similarity index 100% rename from src/doc/docs/guide/diagnostic/jersey-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/jersey-report.md diff --git a/src/doc/docs/guide/diagnostic/lifecycle-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/lifecycle-report.md similarity index 100% rename from src/doc/docs/guide/diagnostic/lifecycle-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/lifecycle-report.md diff --git a/dropwizard-guicey/src/doc/docs/guide/diagnostic/shared-state-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/shared-state-report.md new file mode 100644 index 000000000..260b76e08 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/shared-state-report.md @@ -0,0 +1,37 @@ +# Shared state usage + +Guicey [shared state](../shared.md) is a bundle communication mechanism and safe "static" access +for the important objects (quite rarely required). Before, it was not clear the real sequence of state +population and access, and now there is a special report showing all state manipulations: + +```java +GuiceBundle.builder() + .printSharedStateUsage() +``` + +``` +INFO [2025-03-27 09:49:35,219] ru.vyarus.dropwizard.guice.debug.SharedStateDiagnostic: Shared configuration state usage: + + SET Options (ru.vyarus.dropwizard.guice.module.context.option) at r.v.d.g.m.context.(ConfigurationContext.java:167) + + SET List (java.util) at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60) + MISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:56) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:57) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:61) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:62) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:73) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:74) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:82) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:84) + + SET Bootstrap (io.dropwizard.core.setup) at r.v.d.g.m.context.(ConfigurationContext.java:806) + + SET Map (java.util) at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97) + MISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:93) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:94) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:98) + GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:101) + ... +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/diagnostic/startup-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/startup-report.md new file mode 100644 index 000000000..0d3890298 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/startup-report.md @@ -0,0 +1,67 @@ +# Startup times + +The report intended to show the entire application startup time information to simplify +searching for bottlenecks. It's hard to measure everything exactly from a bundle, +but the report will try to show the time spent in each phase (init, run, web) and time of each +registered dropwizard bundle. + +```java +GuiceBundle.builder() + .printStartupTime() +``` + +Sample output: + +``` +INFO [2025-03-27 09:12:27,435] ru.vyarus.dropwizard.guice.debug.StartupTimeDiagnostic: Application startup time: + + JVM time before : 1055 ms + + Application startup : 807 ms + Dropwizard initialization : 127 ms + GuiceBundle : 123 ms (finished since start at 127 ms) + Bundle builder time : 38 ms + Hooks processing : 3.23 ms + StartupDiagnosticTest$Test1$$Lambda/0x0000711de72a1d70: 2.37 ms + Classpath scan : 44 ms + Commands processing : 4.41 ms + DummyCommand : 0.42 ms + NonInjactableCommand : 3.16 ms + Bundles lookup : 1.15 ms + Guicey bundles init : 3.24 ms + WebInstallersBundle : 0.52 ms + CoreInstallersBundle : 1.83 ms + Installers time : 21 ms + Installers resolution : 15 ms + Scanned extensions recognition : 6.13 ms + Listeners time : 1.35 ms + ConfigurationHooksProcessedEvent : 0.23 ms + BeforeInitEvent : 0.59 ms + BundlesResolvedEvent : 0.009 ms + BundlesInitializedEvent : 0.43 ms + CommandsResolvedEvent : 0.006 ms + InstallersResolvedEvent : 0.01 ms + ClasspathExtensionsResolvedEvent : 0.009 ms + InitializedEvent : 0.007 ms + + Dropwizard run : 679 ms + Configuration and Environment : 483 ms + GuiceBundle : 196 ms + Configuration analysis : 20 ms + ... +``` + +!!! note "Limitations" + * Can't show init time of dropwizard bundles, registered before the guice bundle (obviously) + * `Applicaion#run` method time measured as part of "web" (the bundle can't see this point, but should not be a problem) + +The report could be also enabled for compiled application: `-Dguicey.hooks=startup-time` + +## Before time + +The report can't know what was happening before application initialization (jvm startup time), +but this is usually a meaningful time (while real person waits for application startup). + +So all time before application is indicated with (value obtained from MX bean): + +`JVM time before : 1055 ms` \ No newline at end of file diff --git a/src/doc/docs/guide/diagnostic/web-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/web-report.md similarity index 99% rename from src/doc/docs/guide/diagnostic/web-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/web-report.md index 0f3242726..0c7e5a53f 100644 --- a/src/doc/docs/guide/diagnostic/web-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/web-report.md @@ -89,7 +89,7 @@ If filter is applied by servlet name then it would be rendered *below* target se ``` !!! warning - Filters, applied by servlet name are not shown at all if target servets are not registered. + Filters, applied by servlet name are not shown at all if target servlets are not registered. If filter or servlet is applied with multiple target urls then each pattern will start on new line and only on first line complete information will be shown (idem `--"--` string will be used to identify same filter): @@ -115,7 +115,7 @@ Guice servlets and filters (declared in `ServletModule`s) are shown below guice Guice servlets and filters are shown in both admin and main contexts, because `GuiceFilter` is applied on both contexts and so all urls will work in both contexts. -Note that regex registrations are explicitly markerd with `reges` +Note that regex registrations are explicitly marked with `reges` ```java filterRegex("/1/abc?/.*").through(GRegexFilter.class) @@ -177,4 +177,3 @@ listen(new WebMappingsDiagnostic(new MappingsConfig() Report rendering logic may also be used directly as report provide separate renderer object implementing `ReportRenderer`. Renderer not bound to guice context and assume direct instantiation. - \ No newline at end of file diff --git a/src/doc/docs/guide/diagnostic/yaml-values-report.md b/dropwizard-guicey/src/doc/docs/guide/diagnostic/yaml-values-report.md similarity index 94% rename from src/doc/docs/guide/diagnostic/yaml-values-report.md rename to dropwizard-guicey/src/doc/docs/guide/diagnostic/yaml-values-report.md index 34ed403fb..a166a430a 100644 --- a/src/doc/docs/guide/diagnostic/yaml-values-report.md +++ b/dropwizard-guicey/src/doc/docs/guide/diagnostic/yaml-values-report.md @@ -12,7 +12,7 @@ GuiceBundle.builder() !!! note Even if custom binding report selected (`printCustomConfigurationBindings()`), guicey will always - bind all bindings, including dropwziard `Configuration` class. Custom config report could just shows + bind all bindings, including dropwizard `Configuration` class. Custom config report could just shows less for simplicity. Will print: @@ -169,7 +169,7 @@ INFO [2018-06-18 05:55:03,532] ru.vyarus.dropwizard.guice.module.yaml.report.De @Config("server.uid") Integer = null @Config("server.umask") String = null @Config("server.user") String = null -``` +``` Here you can see: @@ -178,10 +178,22 @@ Here you can see: * Unique sub configuration objects * Values bound to guice context +Also, if [manual qualification](../yaml-values.md#qualified-bindings) is used, all annotated properties would also be shown: + +``` + Qualified bindings: + @Named("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} (metrics) + @CustomQualifier SubObj = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifierSampleTest$SubObj@19e0dffe (obj1) + @Named("sub-prop") Set = (aggregated values) + String = "2" (obj1.prop2) + String = "3" (obj1.prop3) + @Named("custom") String = "1" (prop1) +``` + ## Guice Report is mostly intended to be used to see available guice bindings and that's why -`@Config` annotation is shown almsot everywhere. For example, +`@Config` annotation is shown almost everywhere. For example, ``` @Config("server.serverPush.enabled") Boolean = false @@ -255,4 +267,3 @@ implementing `ReportRenderer`. Renderer not bound to guice context and assume di For examples of direct renderer usage see [events](../events.md) implementation: * `RunPhaseEvent.renderConfigurationBindings()` - diff --git a/src/doc/docs/guide/disables.md b/dropwizard-guicey/src/doc/docs/guide/disables.md similarity index 91% rename from src/doc/docs/guide/disables.md rename to dropwizard-guicey/src/doc/docs/guide/disables.md index bf58f75e0..9bcb3c614 100644 --- a/src/doc/docs/guide/disables.md +++ b/dropwizard-guicey/src/doc/docs/guide/disables.md @@ -23,7 +23,7 @@ simply disabled to avoid installation. Disables are available in [main bundle](configuration.md#main-bundle) and in [guicey bundles](configuration.md#guicey-bundle). !!! warning - Disable is performed by class, so disabling modules and bundles disables all instances of tipe. + Disable is performed by class, so disabling modules and bundles disables all instances of type. The only way to disable exact instance is to use [disable by predicate](#disable-by-predicate). ## Disable extensions @@ -190,9 +190,27 @@ import static ru.vyarus.dropwizard.guice.module.context.Disables.* Simply disable items by type. +Disable extensions, installed by the exact installer: + +```java +@EnableHook +static GuiceyConfigurationHook hook = builder -> + builder.disable(installedBy(WebFilterInstaller.class)); +``` + The condition is java `Predicate`. Use `Predicate#and(Predicate)`, `Predicate#or(Predicate)` and `Predicate#negate()` to compose complex conditions from simple ones. +There are disable shortcuts for exact items type (`Disables.module()`, `Disabled.extension()`, etc.) now raise predicate +type to simplify chained usage: + +```java +builder.disable(module().and(ModuleItemInfo mod -> ! mod.isOverriding())); +``` + +There are special shortcuts for web and jersey extensions (jersey extension is also a web extension): +`Disables.jerseyExtension()` and `Disables.webExtension()` (servlets, filters and jersey). + Most common predicates could be build with `ru.vyarus.dropwizard.guice.module.context.Disables` utility (examples above). @@ -209,4 +227,4 @@ For example: ... ├── installer -LifeCycleInstaller (r.v.d.g.m.i.feature) *DISABLED -``` \ No newline at end of file +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/events.md b/dropwizard-guicey/src/doc/docs/guide/events.md new file mode 100644 index 000000000..db45d9bf6 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/events.md @@ -0,0 +1,142 @@ +# Guicey lifecycle events + +Guicey broadcast lifecycle events in all major points. Each event +provides access to all available state at this point. + +Events could be used for configuration analysis, reporting or to add some special +post processing for configuration items (e.g. post process modules before injector creation). + +!!! important + Event listeners could not modify configuration itself + (can't add new extensions, installers, bundles or disable anything). + +## Events + +All events are listed in `GuiceyLifecycle` enum (in execution order). + + +Event | Description | Possible usage +------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------- +**Dropwizard initialization phase** | | +ConfigurationHooksProcessed^**?**^ | Called after all registered hooks processing. Not called when no hooks used. | Only for info +DropwizardBundlesInitialized^**?**^ | Called after dropwizard bundles initialization (for dropwizard bundles registered through guicey api). Not called if no bundles were registered. | Logging, bundle instances modification (to affect run method) +BundlesFromLookupResolved^**?**^ | Called after resolution bundles through lookup mechanism. Not called if no bundles found. | Logging or post processing of found bundles. +BundlesResolved | Called with all known top-level bundles (transitive bundles are not yet known). Always called to indicate configuration state. | Could be used to modify top-level bundle instances +BundlesInitialized^**?**^ | Called after all bundles initialization (including transitive, so list of bundles could be bigger). Not called when no bundles registered. | Logging, post processing +CommandsResolved^**?**^ | Called if commands search is enabled and at least one command found | Logging +InstallersResolved | Called when all configured (and resolved by classpath scan) installers initialized | Potentially could be used to configure installer instances +ManualExtensionsValidated^**?**^ | Called when all manually registered extension classes are recognized by installers (validated). But only extensions, known to be enabled at that time are actually validated (this way it is possible to exclude extensions for non existing installers). Called only if at least one manual extension registered. | Logging, assertions +ClasspathExtensionsResolved^**?**^ | Called when classes from classpath scan analyzed and all extensions detected (if extension is also registered manually it would be also counted as from classpath scan). Called only if classpath scan is enabled and at least one extension detected. | Logging, assertions +Initialized | Meta event, called after GuiceBundle initialization (most of configuration done). Pure marker event, indicating guicey work finished under dropwizard configuration phase. | Last chance to modify Bootstrap +**Dropwizard run phase** | | +BeforeRun | Meta event, called before any guicey actions just to indicate first point where Environment, Configuration and introspected configuration are available | For example, used by `bundle.printConfigurationBindings()` to print configuration bindings before injector start (help with missed bindings debug) | +BundlesStarted^**?**^ | Called after bundles start (run method call). Not called if no bundles were used at all. Called only if bindings analysis is not disabled. | Logging +ModulesAnalyzed | Called after guice modules analysis and repackaging. Reveals all detected extensions and removed bindings info. | Logging, analysis validation logic +ExtensionsResolved | Called to indicate all enabled extensions (manual, from classpath scan and modules). Always called to indicate configuration state. | Logging or remembering list of all enabled extensions (classes only) +InjectorCreation | Called just before guice injector creation. Provides all configured modules (main and override) and all disabled modules. Always called. | Logging. Note that it is useless to modify module instance here, because they were already processed. +**Guice injector created** | | +ExtensionsInstalledBy | Called when installer installed all related extensions (for each installer) and only for installers actually performed installations (extensions list never empty). Note: jersey extensions are processed later. | Logging of installed extensions. Extension instance could be obtained from injector and post processed. +ExtensionsInstalled^**?**^ | Called after all installers install related extensions. Not called when no installed extensions (nothing registered or all disabled) | Logging or extensions post processing +ApplicationRun | Meta event, called when guice injector started, extensions installed (except jersey extensions because neither jersey nor jetty would be started yet) and all guice singletons initialized. At this point injection to registered commands is performed (this may be important if custom command run application instead of "server"). Point is just before `Application.run` method. | Ideal point for jersey and jetty listeners installation (with shortcut methods in event). +**Jersey initialization** | | +ApplicationStarting | Meta event, called after application run method, but before web server startup (called for lightweight tests) | It is used for starting lightweight jersey context (in stub rest). +JerseyConfiguration | Jersey context starting. Both jersey and jetty are starting. | First point where jersey's `InjectionManager` (and `ServiceLocator`) become available +JerseyExtensionsInstalledBy | Called when jersey installer installed all related extensions (for each installer) and only for installers actually performed installations (extensions list never empty) | Logging of installed extensions. Extension instance could be obtained from injector/locator and post processed. +JerseyExtensionsInstalled^**?**^ | Called after all jersey installers install related extensions. Not called when no installed extensions (nothing registered or all disabled). At this point HK2 is not completely started yet (and so extensions) | Logging or extensions post processing +ApplicationStarted | Meta event, called after complete dropwizard startup. This event also will be fired in guicey lightweight tests | May be used as assured "started" point (after all initializations). For example, for reporting. +ApplicationShutdown | Meta event, called on server shutdown start. This event also will be fired in guicey lightweight tests | May be used for shutdown logic. +ApplicationStoppedEvent | Meta event, called after application shutdown. This event also will be fired in guicey lightweight tests | May be used in rare cases to cleanup fs resources after application stop. + +^?^ - event may not be called + +## Listeners + +Events listener registration: + +```java +GuiceBundle.builder() + .listen(new MyListener(), new MyOtherListener()) + ... + .build() +``` + +!!! note + Listeners could be also registered in guicey bundle, but they will not receive all events: + + * `>= BundlesInitialized` for listeners registered in initialization method + * `>= BundlesStarted` for listeners registered in run method + +Event listener could implement generic event interface `GuiceyLifecycleListener` and use +enum to differentiate required events: + +```java +public class MyListener implements GuiceyLifecycleListener { + + public void onEvent(GuiceyLifecycleEvent event) { + switch (event.getType()) { + case InjectorCreation: + InjectorCreationEvent e = (InjectorCreationEvent) event; + ... + } + } +} +``` + +Or use `GuiceyLifecycleAdapter` adapter and override only required methods: + +```java +public class MyListener extends GuiceyLifecycleAdapter { + + @Override + protected void injectorCreation(final InjectorCreationEvent event) { + ... + } +} +``` + +!!! tip + In `ApplicationStarted` and `ApplicationShutdown` events lightweight guicey test + environment may be differentiated from real server startup with `.isJettyStarted()` method. + +### De-duplication + +Event listeners are also support de-duplication to prevent unnecessary duplicates usage +(for example, two bundles may register one listener because they are not always used together). +But it is **not the same mechanism** as configuration items de-duplication. + +Simply listeners are registered in the `LinkedHashSet` and so listeners could control de-duplication +with a proper `equals` and `hashCode` implementations + +Many reports use this feature (because all of them are based on listeners). For example, +[diagnostic report](diagnostic/configuration-report.md) use the following implementations: + +```java +@Override +public boolean equals(final Object obj) { + // allow only one instance with the same title + return obj instanceof ConfigurationDiagnostic + && reportTitle.equals(((ConfigurationDiagnostic) obj).reportTitle); +} + +@Override +public int hashCode() { + return reportTitle.hashCode(); +} +``` + +And with it, `.printDiagnosticInfo()` can be called multiple times and still only one report +will be actually printed. + +### Events hierarchy + +All event classes inherit from some base event classes. Base event classes are extending each other: +as lifecycle phases go, more objects become available. So you can access any available (at this point) object +from event instance. + +Base event | Description +-----------|------------- +GuiceyLifecycleEvent | The lowest event type. Provides access to event type and options. +ConfigurationPhaseEvent | Initialization phase event. Provides access to Bootstrap. +RunPhaseEvent | Dropwizard run phase. Provides access to Configuration, ConfigurationTree, Environment. Shortcut for configuration bindings report renderer +InjectorPhaseEvent | Guice injector created. Available injector and GuiceyConfigurationInfo (guicey configuration). Shortcuts for configuration reports renderer +JerseyPhaseEvent | Jersey starting. Jersey's `InjectionManager` available. diff --git a/src/doc/docs/guide/extensions.md b/dropwizard-guicey/src/doc/docs/guide/extensions.md similarity index 98% rename from src/doc/docs/guide/extensions.md rename to dropwizard-guicey/src/doc/docs/guide/extensions.md index 59309bc23..7cf25fec2 100644 --- a/src/doc/docs/guide/extensions.md +++ b/dropwizard-guicey/src/doc/docs/guide/extensions.md @@ -141,7 +141,7 @@ used because it would be impossible to automatically register health check witho ## Jersey extensions -All jersey extensions are [recognized](../installers/jersey-ext.md) by `javax.ws.rs.ext.Provider` jersey annotation. +All jersey extensions are [recognized](../installers/jersey-ext.md) by `jakarta.ws.rs.ext.Provider` jersey annotation. There are [many extensions](../installers/jersey-ext.md) supported. ```java diff --git a/src/doc/docs/guide/guice/bindings.md b/dropwizard-guicey/src/doc/docs/guide/guice/bindings.md similarity index 92% rename from src/doc/docs/guide/guice/bindings.md rename to dropwizard-guicey/src/doc/docs/guide/guice/bindings.md index 1ad6fad52..c2a9dcc47 100644 --- a/src/doc/docs/guide/guice/bindings.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/bindings.md @@ -6,7 +6,7 @@ Guicey always installs `GuiceBootstrapModule` which registers the following bind * `io.dropwizard.setup.Bootstrap` * `io.dropwizard.Configuration` * `io.dropwizard.setup.Environment` -* Detailed [configuration bindings](#configuration) (by root classes, interfaces, yaml path or unique sub type) +* Detailed [configuration bindings](#configuration) (by root classes, interfaces, yaml path or unique subtype) * [Jersey objects](#jersey-specific-bindings) (including [request scoped](#request-and-response)) * Guicey [special objects](#guicey-configuration) * All installed [extensions](#extension-bindings) @@ -121,7 +121,7 @@ Which could be injected directly: ### Value by path -All visible configuration paths values are directly bindable: +All visible configuration paths values can be directly bound: ```java public class MyConfig extends Configuration { @@ -148,7 +148,7 @@ public class SubConf { !!! note Generified types are bound only with generics (with all available type information). - If you will have `SubConf sub` in config, then it will be bound with correct generic `SubConfig` + If you have `SubConf sub` in config, then it will be bound with correct generic `SubConfig` (suppose generic T is declared as String). Value type, **declared in configuration class** is used for binding, but there are two exceptions. @@ -216,18 +216,18 @@ so use `Provider` to inject these bindings. These bindings available after HK2 context start: -* `javax.ws.rs.core.Application` -* `javax.ws.rs.ext.Providers` +* `jakarta.ws.rs.core.Application` +* `jakarta.ws.rs.ext.Providers` * `org.glassfish.hk2.api.ServiceLocator` * `org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider` Request-scoped bindings: -* `javax.ws.rs.core.UriInfo` -* `javax.ws.rs.container.ResourceInfo` -* `javax.ws.rs.core.HttpHeaders` -* `javax.ws.rs.core.SecurityContext` -* `javax.ws.rs.core.Request` +* `jakarta.ws.rs.core.UriInfo` +* `jakarta.ws.rs.container.ResourceInfo` +* `jakarta.ws.rs.core.HttpHeaders` +* `jakarta.ws.rs.core.SecurityContext` +* `jakarta.ws.rs.core.Request` * `org.glassfish.jersey.server.ContainerRequest` * `org.glassfish.jersey.server.AsyncContext` @@ -242,8 +242,8 @@ request and response objects and use under filter, servlet or resources calls (g If you disable guice filter with [.noGuiceFilter()](servletmodule.md) then guicey will bridge objects from HK2 context: -* `javax.servlet.http.HttpServletRequest` -* `javax.servlet.http.HttpServletResponse` +* `jakarta.servlet.http.HttpServletRequest` +* `jakarta.servlet.http.HttpServletResponse` !!! attention "" This means you can still inject them, but request and response will @@ -289,14 +289,13 @@ to [guicey configuration details](../diagnostic/configuration-report.md): for application, but this section is important in case of problems. In order to support guice `binder().requireExplicitBindings()` option guicey binds -all extensions with untargetted binding: `binder().bind(YourExtension.class)`. +all extensions with untargeted binding: `binder().bind(YourExtension.class)`. But there are three exceptions: -* Installers with custom binding logic (like [plugins installer](../../installers/plugin.md)) +* Installers with custom binding logic (like a [plugins installer](../../installers/plugin.md)) * If extension was detected from binding (obviously binding already exists) * If extension is annotated with`@LazyBinding` As injector is created in `Stage.PRODUCTION`, all singleton extensions will be instantiated in time of injector startup (injector stage could be changed in [main bundle](../configuration.md#main-bundle)). - \ No newline at end of file diff --git a/src/doc/docs/guide/guice/injector.md b/dropwizard-guicey/src/doc/docs/guide/guice/injector.md similarity index 86% rename from src/doc/docs/guide/guice/injector.md rename to dropwizard-guicey/src/doc/docs/guide/guice/injector.md index 6f7c75819..7a0380b84 100644 --- a/src/doc/docs/guide/guice/injector.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/injector.md @@ -64,10 +64,29 @@ Inside guice context you can simply inject Injector instance: @Inject Injector injector; ``` +### Application injections + +To simplify guice beans access in `Application#run` method, you can use +injections directly in application class: + +```java +public class App extends Application { + + @Inject MyService service; + + ... + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + service.doSomething(); + } +} +``` + ## Injector stage By default injector is created at `PRODICTION` stage, which means that all registered -singletons are instantiated in time of injector craetion. +singletons are instantiated in time of injector creation. You can change stage at [main bundle](../configuration.md#injector): @@ -103,4 +122,4 @@ Custom injector factory could be registered in guice bundle builder: bootstrap.addBundle(GuiceBundle.builder() .injectorFactory(new CustomInjectorFactory()) ... -``` \ No newline at end of file +``` diff --git a/src/doc/docs/guide/guice/module-analysis.md b/dropwizard-guicey/src/doc/docs/guide/guice/module-analysis.md similarity index 83% rename from src/doc/docs/guide/guice/module-analysis.md rename to dropwizard-guicey/src/doc/docs/guide/guice/module-analysis.md index 2be0c8963..20c3c7955 100644 --- a/src/doc/docs/guide/guice/module-analysis.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/module-analysis.md @@ -41,9 +41,9 @@ public class MyModule extends AbstractModule { Guicey will detect `MyResource` as jersey resource and `MyManaged` as managed extension. !!! tip - Extensions annotated with `@InvisibleForScanner` are not recognized, like in [clsspath scanner](../scan.md). + Extensions annotated with `@InvisibleForScanner` are not recognized, like in [classpath scanner](../scan.md). But note that annotated extensions *should not be registered manually*! Because it will - lead to default extension binding registration by guicey, which will most likley conflict with + lead to default extension binding registration by guicey, which will most likely conflict with existing binding (as a workaround `@LazyBinding` annotation may be used). Alternatively, you can simply qualify bean and it would not be recognized as extension. @@ -73,7 +73,7 @@ GuiceBundle.builder() !!! success "Will be recognized" ```java - // untargetted binding + // untargeted binding bind(Extension.class) // left side of the link @@ -106,7 +106,7 @@ GuiceBundle.builder() Qualified and generified cases are not supported because they imply that multiple instances of one class may be declared. This rise problems with direct manual declaration: for example, if user declare `.extensions(Extension.class)` and in module we have - `bind(Extension.class).annotatedWith(Qualify.class)` how can we be sure if its the same + `bind(Extension.class).annotatedWith(Qualify.class)` how can we be sure if it is the same declaration or not? Current implementation will not revognize qualified extension and automatically create @@ -134,16 +134,55 @@ When `Extension` disabled `One-->Two` link is also removed. Motivation: -* First of all, this avoid error cases when remaining chain part contains +* First of all, this avoids error cases when remaining chain part contains only abstract types (e.g. only interfaces remains) -* Removes possible incosistencies as long chains may appear due to some class overrides and so +* Removes possible inconsistencies as long chains may appear due to some class overrides and so removing only top (overriding) class will just to "before override" state. Removed chains are visible on [guice report](#removed-bindings). +### Private modules + +Guicey will also search extensions in private modules (of course, only in exposed beans). + +```java +public class PModule extends PrivateModule { + @Override + protected void configure() { + // ExtImpl is extension (recognition sign absent in interface) + bind(IExt.class).to(ExtImpl.class); + // extension exposed by interface + expose(IExt.class); + } +} + +public interface IExt {... } +public class Ext implements IExt, Managed { ... } +``` + +Guicey would detect that `ExtImpl` is an extension, and it is available (through exposed interface) +and so register it as an extension. + +!!! important + Guicey rely on extension classes and so it would need direct extension access (to be able to call + `Injector.getInstance(ExtImpl.class)`). By default, it is not possible (exposed only interface), + but guicey would change private module: it would add an additional `expose` for `ExtImpl`. + +Also, as any guicey extension could be disabled, then `.disable(ExtImpl.class)` +would remove binding inside private module (works only for top-level private modules). + +If you'll face any problems with private modules behavior, **please report it**. + +Private modules analysis could be disabled with: + +```java +GuiceBundle.builder() + .option(GuiceyOptions.AnalyzePrivateGuiceModules, false) +``` + ## Transitive modules -During bindings analysis guicey can see binding modules hierarchy (module A install module B which register binding C). +During bindings analysis guicey can see binding module's hierarchy (module "A" installs module "B", which registers binding C). Using this guicey can remove all bindings relative to exact module class - the result is the same as if such module was never registered. @@ -165,7 +204,7 @@ To completely switch off analysis use option: With disabled analysis [injector factory](injector.md) will receive user provided modules directly (instead of pre-parsed synthetic module). !!! important - Enabled analysis completely prevent situations when default binding, created by guciey, conflict + Enabled analysis completely prevent situations when default binding, created by guicey, conflict with manual binding existing in module. In such case startup will fail. Before modules analysis it was only possible to solve such issue with `@LazyBinding` annotation. @@ -247,4 +286,4 @@ Removed chains are shown as: ``` BINDING CHAINS └── Base --[linked]--> Ext --[linked]--> ExtImpl *CHAIN REMOVED -``` \ No newline at end of file +``` diff --git a/src/doc/docs/guide/guice/module-autowiring.md b/dropwizard-guicey/src/doc/docs/guide/guice/module-autowiring.md similarity index 97% rename from src/doc/docs/guide/guice/module-autowiring.md rename to dropwizard-guicey/src/doc/docs/guide/guice/module-autowiring.md index f5d55fbe5..b62521ebe 100644 --- a/src/doc/docs/guide/guice/module-autowiring.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/module-autowiring.md @@ -53,6 +53,8 @@ public class MyModule extends DropwizardAwareModule { confuguration(Class) // unique sub configuration configuration(String) // configuration value by yaml path configurations(Class) // sub configuration objects by type (including subtypes) + annotatedConfiguration(ann) // annotaed configuration value by instance + annotatedConfiguration(Class) // annotaed configuration value by annotation type options() // access guicey options sharedState(Class) // shared sctate access } diff --git a/src/doc/docs/guide/guice/override.md b/dropwizard-guicey/src/doc/docs/guide/guice/override.md similarity index 94% rename from src/doc/docs/guide/guice/override.md rename to dropwizard-guicey/src/doc/docs/guide/guice/override.md index 464b4e1f7..e8f6041ec 100644 --- a/src/doc/docs/guide/guice/override.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/override.md @@ -22,7 +22,7 @@ public class MyModule extends AbstractModule { Generally have few options: -* If it implement interface, implement your own service and bind as +* If it implements an interface, implement your own service and bind as `#!java bind(ServiceX.class).to(MyServiceXImpl.class)` * If service is a class, you can modify its behaviour with extended class `#!java bind(ServiceX.class).to(MyServiceXExt.class)` @@ -58,4 +58,4 @@ And everywhere in code `#!java @Inject ServiceX service;` will receive `CustomSe !!! note Overriding module could contain additional bindings - they would be also available in the resulted injector. - (binding from overriding module either overrides existing binding or simply added as new binding) \ No newline at end of file + (binding from overriding module either overrides existing binding or simply added as new binding) diff --git a/src/doc/docs/guide/guice/scopes.md b/dropwizard-guicey/src/doc/docs/guide/guice/scopes.md similarity index 67% rename from src/doc/docs/guide/guice/scopes.md rename to dropwizard-guicey/src/doc/docs/guide/guice/scopes.md index 8f57d6963..fbef854a2 100644 --- a/src/doc/docs/guide/guice/scopes.md +++ b/dropwizard-guicey/src/doc/docs/guide/guice/scopes.md @@ -41,7 +41,7 @@ public class MyResource {} ## Singleton -Both `com.google.inject.Singleton` and `javax.inject.Singleton` annotations could be used. +Both `com.google.inject.Singleton` and `jakarta.inject.Singleton` annotations could be used. !!! tip Prefer declaring `@Singleton` scope on all beans, except cases when different scope is required. @@ -49,7 +49,7 @@ Both `com.google.inject.Singleton` and `javax.inject.Singleton` annotations coul ## Request By default, `GuiceFilter` is registered for both application and admin contexts. -And so request (and session) scopes will be be available in both contexts. +And so request (and session) scopes will be available in both contexts. ```java @RequestScoped @@ -117,6 +117,62 @@ public class RequestBean { Such additional call is not required for pure guice-managed request scope objects. +Note that you **can't** use scope inside the scope (e.g. call one transferRequest action inside another). +If you need to transfer scope to another sub-thread (3rd thread), make sure that +the transferRequest action will be called after the current action closes. + +!!! hint + The current scope object is stored in thread locale (for http it's `GuiceFilter.localContext`). + Each time you call `ServletScopes.transferRequest` it gets this context from thread local. + This means all transferRequest actions, created in the current thread (or inside such action) + will use THE SAME context instance. + + There is a simple `ReentrantLock` in context which prevents simultaneous context usage + from multiple threads (locks scope opening). So if you're going to spawn and wait for another thread, calling + transferRequest actions inside current action, you'll get a dead lock. + + Pay attention, that spawing a new thread, using request context from current scope is + completely normal as long as you don't wait for the result (lock will release as soon as + your current context will close). + +If you need to spawn a new thread (requiring request scope) and wait for its result within `trasferRequest` scope, +you can prepare several transfer actions ahead of time (separate action for each thread): + +```java +// action for the first thread +final Callable action1 = ServletScopes.transferRequest(...); +// action for the sub-furst thread +final Callable action2 = ServletScopes.transferRequest(...); + +// note: both actions share the same context instance + +CompletableFuture.supplyAsync(() -> { + action1.call(); + CompletableFuture.supplyAsync(() -> { + action2.call(); + }).join(); +}).join() +``` + +or returning another scoped action from the first one (if the first thread result must be used in the third thread): + +```java +final Callable> action1 = ServletScopes.transferRequest(() -> { + // do something and then action for sub-thread + return ServletScopes.transferRequest(...); +}); + +// in this case the context instance will also be THE SAME + +CompletableFuture.supplyAsync(() -> { + final Callable action2 = action1.call(); + CompletableFuture.supplyAsync(() -> { + action2.call(); + }).join(); +}).join() +``` + + ### Request scope simulation Sometimes, request scoped beans may need to be used somewhere without request (for example, @@ -150,4 +206,4 @@ To always start beans (even in `DEVELOPMENT` stage) guice provide eager singleto For cases when you don't want to manually declare bean, but require it to start with guice context you can either implement [Managed](../../installers/managed.md) or mark bean as [@EagerSingleton](../../installers/eager.md) (the latter will simply bind annotated bean as -guice eager singleton instead of you). \ No newline at end of file +guice eager singleton instead of you). diff --git a/src/doc/docs/guide/guice/servletmodule.md b/dropwizard-guicey/src/doc/docs/guide/guice/servletmodule.md similarity index 100% rename from src/doc/docs/guide/guice/servletmodule.md rename to dropwizard-guicey/src/doc/docs/guide/guice/servletmodule.md diff --git a/src/doc/docs/guide/hk2.md b/dropwizard-guicey/src/doc/docs/guide/hk2.md similarity index 91% rename from src/doc/docs/guide/hk2.md rename to dropwizard-guicey/src/doc/docs/guide/hk2.md index 2256b1ca7..c4325d44f 100644 --- a/src/doc/docs/guide/hk2.md +++ b/dropwizard-guicey/src/doc/docs/guide/hk2.md @@ -1,11 +1,15 @@ # HK2 !!! danger - Guicey will get rid of HK2 usage completely in the next version and all + Someday guicey will get rid of HK2 usage completely, which means HK2-related api and features would be removed. + + But, as it requires a lot of efforts, all HK2-related apis and **deprecated softly** + not - this means, there are no direct deprecation and only + javadoc mention "soft deprecation". Please try to avoid using HK2 at all. - All api supposed to be removed is marked as deprecated now. But there are no - replacemenets provided. Please try to avoid using HK2 at all. + Previous strong deprecation was removed because there are no + replacemenets provided for current api (and its not clear when complete removal whill happen). By default, guicey manage all extensions under guice context and only register extensions in HK2 by instance. Normally you shouldn't know about HK2 at all. diff --git a/src/doc/docs/guide/hooks.md b/dropwizard-guicey/src/doc/docs/guide/hooks.md similarity index 94% rename from src/doc/docs/guide/hooks.md rename to dropwizard-guicey/src/doc/docs/guide/hooks.md index e6b1a859b..4e7fbc18d 100644 --- a/src/doc/docs/guide/hooks.md +++ b/dropwizard-guicey/src/doc/docs/guide/hooks.md @@ -5,7 +5,7 @@ Guicey provides special mechanism for external configuration: ```java public class MyHook implements GuiceyConfigurationHook { @Override - public void configure(GuiceBundle.Builder builder) { + public void configure(GuiceBundle.Builder builder) throws Exception { builder.bundles(new AdditinoalBundle()); } } @@ -86,7 +86,7 @@ INFO [2019-09-16 16:26:35,229] ru.vyarus.dropwizard.guice.hook.ConfigurationHoo !!! note By default, guicey register [diagnostic hook](diagnostic/diagnostic-tools.md#diagnostic-hook) - to easily activate dignostic reports on compiled application: + to easily activate diagnostic reports on compiled application: ``` -Dguicey.hooks=diagnistic - ``` \ No newline at end of file + ``` diff --git a/src/doc/docs/guide/installers.md b/dropwizard-guicey/src/doc/docs/guide/installers.md similarity index 99% rename from src/doc/docs/guide/installers.md rename to dropwizard-guicey/src/doc/docs/guide/installers.md index 7c23bc5d8..0f31332dc 100644 --- a/src/doc/docs/guide/installers.md +++ b/dropwizard-guicey/src/doc/docs/guide/installers.md @@ -1,6 +1,6 @@ # Installers -Installer is a core integration concept: every extension point has it's own installer. +Installer is a core integration concept: every extension point has its own installer. Installers are registered manually or detected by [classpath scan](scan.md). ## Default installers @@ -145,7 +145,7 @@ public class ScheduledInstaller implements FeatureInstaller, ``` Report method [will be called automatically](#reporting) after all extensions installation. -More complex installers may require special reporter (like jersey extensins installer). +More complex installers may require special reporter (like jersey extensions installer). Another example, suppose `CustomFeature` is a base class for our jersey extensions. diff --git a/src/doc/docs/guide/lifecycle.md b/dropwizard-guicey/src/doc/docs/guide/lifecycle.md similarity index 95% rename from src/doc/docs/guide/lifecycle.md rename to dropwizard-guicey/src/doc/docs/guide/lifecycle.md index 9022794e8..a59ff244d 100644 --- a/src/doc/docs/guide/lifecycle.md +++ b/dropwizard-guicey/src/doc/docs/guide/lifecycle.md @@ -35,7 +35,7 @@ them later for detection. * Search [for commands](commands.md#automatic-installation) (if classpath scan enabled) * Prepare [installers](installers.md): - Detect installers with classpath scan (if configured) - - Instantiate [not diabled](disables.md#disable-installers) installers + - Instantiate [not disabled](disables.md#disable-installers) installers * Resolve [extensions](extensions.md): - Validate all [enabled](disables.md#disable-extensions) manually registered extensions: one of prepared installers must recognize extension or error will be thrown. @@ -87,9 +87,8 @@ them later for detection. !!! note Any `EnvironmentCommand` did no start jersey, so managed objects will not be started (but you can start required - services [manually](commands.md#environment-commands). Also, all jersey related extensions will not be started. + services [manually](commands.md#environment-commands)). Also, all jersey related extensions will not be started. Still, core guice context will be completely operable. !!! attention "" When guice context is created, *jersey context doesn't exist* and when jersey context is created *it doesn't aware of guice existence*. - diff --git a/src/doc/docs/guide/modules.md b/dropwizard-guicey/src/doc/docs/guide/modules.md similarity index 51% rename from src/doc/docs/guide/modules.md rename to dropwizard-guicey/src/doc/docs/guide/modules.md index 6e61ee952..7adde0bf8 100644 --- a/src/doc/docs/guide/modules.md +++ b/dropwizard-guicey/src/doc/docs/guide/modules.md @@ -1,21 +1,12 @@ # Modules -All additional guicey integartion modules are maintained as separate project: [dropwizard-guicey-ext](https://github.com/xvik/dropwizard-guicey-ext) - -!!! note - Module versions are based on guicey version: `$guiceyVersion-N`. - For example, 5.0.0-1 means first release of extensions for guicey 5.0.0. - - This convention is commonly used for dropwizard extension modules. Module | Description -------|------------ -[BOM](../extras/bom.md) | Maven BOM for modules and their dependencies [Admin REST](../extras/admin-rest.md) | Admin context rest support. [Lifecycle annotations](../extras/lifecycle-annotations.md) | `@PostConstruct`, `@PostStartup`, `@PreDestroy` support [EventBus](../extras/eventbus.md) | Guava eventbus integration -[JDBI](../extras/jdbi.md) | JDBI integration (based on dropwizard-jdbi) [JDBI3](../extras/jdbi3.md) | JDBI3 integration (based on dropwizard-jdbi3) [SPA](../extras/spa.md) | HTML5 routing support for single page applications [Server pages](../extras/gsp.md) | JSP-like templates support (based on dropwizard-views) -[Validation](../extras/validation.md) | use validation annotations on guice beans (same behaviour as rest) \ No newline at end of file +[Validation](../extras/validation.md) | use validation annotations on guice beans (same behaviour as rest) diff --git a/src/doc/docs/guide/options.md b/dropwizard-guicey/src/doc/docs/guide/options.md similarity index 99% rename from src/doc/docs/guide/options.md rename to dropwizard-guicey/src/doc/docs/guide/options.md index 24097807c..676785bb1 100644 --- a/src/doc/docs/guide/options.md +++ b/dropwizard-guicey/src/doc/docs/guide/options.md @@ -161,7 +161,7 @@ Converter is actually any `java.util.Function` (here, lambda with method call (` ### System properties -As shown before, you can bind single system property to option. But you can allso allow +As shown before, you can bind single system property to option. But you can also allow to set any option with system property: ```java @@ -199,7 +199,7 @@ new OptionsMapper() .map() ``` -When enabled, all mapped options will be printed to console (logger is not used becuase it's not yet initialized). +When enabled, all mapped options will be printed to console (logger is not used because it's not yet initialized). Example output: ``` diff --git a/src/doc/docs/guide/ordering.md b/dropwizard-guicey/src/doc/docs/guide/ordering.md similarity index 100% rename from src/doc/docs/guide/ordering.md rename to dropwizard-guicey/src/doc/docs/guide/ordering.md diff --git a/src/doc/docs/guide/scan.md b/dropwizard-guicey/src/doc/docs/guide/scan.md similarity index 60% rename from src/doc/docs/guide/scan.md rename to dropwizard-guicey/src/doc/docs/guide/scan.md index dae3514f3..a61f4d388 100644 --- a/src/doc/docs/guide/scan.md +++ b/dropwizard-guicey/src/doc/docs/guide/scan.md @@ -1,7 +1,7 @@ # Classpath scan !!! summary - Use scan only for application package. When part of application extracted to it's own library (usually already mature part) + Use scan only for application package. When part of application extracted to its own library (usually already mature part) create [guicey bundle](bundles.md) for it with explicit extensions definition. Use [manual bundles installation](configuration.md) or [bundle lookup mechanism](bundles.md#bundle-lookup) to install custom bundles. @@ -21,6 +21,87 @@ GuiceBundle.builder() .enableAutoConfig("com.mycompany.pkg1", "com.mycompany.pkg2") ``` +If no packages specified, classpath scan would be activated for application package: + +```java +GuiceBundle.builder() + .enableAutoConfig() +``` + +(equivalent to `.enableAutoConfig(getClass().getPackage().getName())` + +### Filter classes + +By default, classpath scanner checks all available classes, and the only way to avoid extension +recognition is `@InvisibleForScanner` annotation. + +Now custom conditions could be specified: + +```java +GuiceBundle.builder() + .autoConfigFilter(ignoreAnnotated(Skip.class)) +``` + +In this example, classes annotated with `@Skip` would not be recognized. + +!!! note + Assumed static import for `ClassFilters` utility, containing the most common cases. + If required, raw predicate could be used: + ```java + .autoConfigFilter(cls -> !cls.isAnnotationPresent(Skip.class)) + ``` + +It is also possible now to implement spring-like approach when only annotated classes +are recognized: + +```java +GuiceBundle.builder() + .autoConfigFilter(annotated(Component.class, Service.class)) +``` + +Here only extensions annotated with `@Component` or `@Service` would be recognized. + +!!! note + This filter affects only extension search: installers and commands search does not use filters + (because it would be confusing and error-prone). + +!!! tip + Multiple filters could be specified: + ```java + GuiceBundle.builder() + .autoConfigFilter(annotated(Component.class, Service.class)) + .autoConfigFilter(ignoreAnnotated(Skip.class)) + ``` + +Auto config filter also affects extensions recognition from guice bindings and so +could be used for ignoring extensions from bindings. + +It is also possible now to exclude some sub-packages from classpath scan: + +```java +GuiceBundle.builder() + .enableAutoConfig("com.company.app") + .autoConfigFilter(ignorePackages("com.company.app.internal")) +``` + +### Private classes + +By default, guicey does not search extensions in protected and package-private classes: + +```java +public class Something { + static class Ext1 implements Managed {} + protected static class Ext2 implements Managed {} +} +``` + +But such extensions could be enabled with: + +```java +GuiceBundle.builder() + .option(GuiceyOptions.ScanProtectedClasses, true) +``` + ## How it works When auto scan enabled: @@ -61,7 +142,7 @@ public static class FooExceptionMapper extends AbstractExceptionMapper usedAssetNames = new ArrayList<>(); + + public void checkUnique(final String assetName) { + checkArgument(!usedAssetNames.contains(assetName), + "SPA with name '%s' is already registered", assetName); + usedAssetNames.add(assetName); + } +} +``` + + +[GSP](../extras/gsp.md) bundles use it for bundles communication. +Core bundle register global configuration: + +```java +public class ServerPagesBundle extends UniqueGuiceyBundle { + @Override + public void initialize(final GuiceyBootstrap bootstrap) { + loadRenderers(); + + // register global config + bootstrap + .shareState(ServerPagesGlobalState.class, config) + } +} +``` + +Application bundle reference this state: + +```java +public class ServerPagesAppBundle implements GuiceyBundle { + @Override + public void initialize(final GuiceyBootstrap bootstrap) { + this.config = bootstrap.sharedStateOrFail(ServerPagesGlobalState.class, + "Either server pages support bundle was not installed (use %s.builder() to create bundle) " + + " or it was installed after '%s' application bundle", + ServerPagesBundle.class.getSimpleName(), app.name); + // register application globally + config.register(app); + } +} +``` + +This way, duplicate registrations could be checked, and global views support could be configured by application bundles. + +## Shared state restrictions + +Internally shared state is a `Map`, but state API force you to use Class as key for **type safety**: +it is assumed that **unique class** would be created for shared state (even if you just need to store a simple value) +and the stored object type would be a key. + +Other restrictions: + +* State value can be **set just once**! This is simply to avoid hard to track problems with overridden state. +* State value can't be null! Again, to avoid problems with NPE errors. + +## Auto-closable values + +If stored value implements `AutoClosable` - it would be closed +automcaticlly on application shutdown. + +## State usage technics + +### Parent-child + +Value stored in one place (some parent bundle): + +```java +bootstrap.shareState(SomeState.class, config); +``` + +And accessed in some other place (other child bundle or module): + +```java +SomeState state = bootstrap + .sharedStateOrFail(SomeState.class, "State not declared"); +``` + +Here, error would be thrown if state is not initialized. + +!!! note + Guicey bundles registration works the same way as for dropwizard bundles: + if you register some bundle from the core bundle, it would be initialized immediately. + + ```java + public class MyBundle implements GuiceyBundle { + public void initialize(GuiceyBootstrap bootstrap) throws Exception { + // assume bundle initialize shared state value + // (this bundle could be a unique bundle so guicey would remove duplicates) + bootstarp.bundles(new ParentBundle()); + // shared value could be used here + SomeState state = bootstrap + .sharedStateOrFail(SomeState.class, "State not declared"); + } + } + ``` + +### Indirect registration + +In some cases, we might have a "race condition" if we're not sure when state value is initialized +(for example, two bundles could be declared in the different order). + +For such cases, there is delayed access: + +```java +bootstrap.whenSharedStateReady(SomeState.class, (state) -> ...) +``` + +!!! important + Listener could be not called at all if target value would not be initialized. + All not used listeners could be seen in [shared state report](diagnostic/shared-state-report.md) + +### First wins + +As shown in the SPA example above, shared state may be used by different instances +of the same bundle to perform global validations. + +In this case "get or initialize" approach used: + +```java +SomeState state = bootstrap.sharedState(SomeState.class, SomeState::new) +``` + +Here existing state would be requested (if already registered) or new one stored. ## Utility @@ -40,26 +176,14 @@ Shared state holds references to the main dropwizard objects, see methods: - getEnvironment() - getConfiguration() - getConfigurationTree() +- getInjector() +- getOptions() All of them return providers: e.g. `SharedConfigurationState.getStartupInstance().getBootsrap()` would return `Provider`. This is required because target object might not be available yet, still there would be a way to initialize some logic with "lazy object" (to call it later, when object would be available) at any configuration stage. -## Shared state restrictions - -Internally shared state is a `Map`. Class is used as key because assumed -usage scope is bundle and it will force you to use bundle class as a key (or any holder object class). Moreover, non string -key reduce dummy typos (internally, values are stored by string class name to unify keys from different class loaders). - -Other restrictions: - -* State value can be **set just once**! This is simply to avoid hard to track problems with overridden state. -* State value can't be null! Again, to avoid problems with NPE errors. - -It is assumed that state will be used not for simple values, but for shared configuration objects. -But there is no direct restrictions. - ## Main bundle It is assumed that there should be no need to access shared state from [main bundle](configuration.md#main-bundle). @@ -68,7 +192,7 @@ So [the only state-related method](configuration.md#hooks-related) actually assu ```java static class XHook implements GuiceyConfigurationHook { @Override - void configure(GuiceBundle.Builder builder) { + void configure(GuiceBundle.Builder builder) throws Exception { builder.withSharedState(state -> { state.put(XHook, new SharedObject()); }); @@ -114,7 +238,7 @@ SharedConfigurationState.get(environment) SharedConfigurationState.lookup(environment, XBundle.class) ``` -Special shorcut methods may be used for "get or fail behaviour": +Special shortcut methods may be used for "get or fail behaviour": ```java SharedConfigurationState.lookupOrFail(app, XBundle.class, @@ -145,6 +269,8 @@ The following objects are available in shared state just in case: * Environment * Configuration * [ConfigurationTree](yaml-values.md) +* [Options](options.md) +* Injector So any of it could be accessed statically with application or environment instance: @@ -161,4 +287,4 @@ Bootstrap bootstrap = SharedConfigurationState ``` !!! tip - During startup these objects might be referenced as lazy objects with [shortcuts](#utility) \ No newline at end of file + During startup these objects might be referenced as lazy objects with [shortcuts](#utility) diff --git a/dropwizard-guicey/src/doc/docs/guide/test/assertj.md b/dropwizard-guicey/src/doc/docs/guide/test/assertj.md new file mode 100644 index 000000000..de12d663d --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/assertj.md @@ -0,0 +1,280 @@ +# AssertJ + +It is highly recommended to use [AssertJ](https://assertj.github.io/doc/) instead of JUnit assertions +(dropwizard team use for testing dropwizard). + +AssertJ version is already managed by dropwizard BOM: + +```groovy +testImplementation 'org.assertj:assertj-core' +``` + +AssertJ assertions are reversed, which might be cumbersome for simple assertions: + +```java +// Junit +Assertions.assertEquals(12, something); + +// AssertJ +Assertions.assertThat(something).isEqualTo(12); +``` + +But, with AssertJ you can combine assertions: + +```java +assertThat(frodo.getName()).startsWith("Fro") + .endsWith("do") + .isEqualToIgnoringCase("frodo"); +``` + +!!! tip + See [assertions guide](https://assertj.github.io/doc/#assertj-core-assertions-guide) + +!!! important + In many cases, assertj assertion fail messages would be much more informative, + which speeds up tests development and regressions investigation. + +## Text assertions + +AssertJ greatly simplifies large text comparisons (e.g. console output). +In Junit, to check if output contains some part you'll have to do: + +```java +Assertions.assertTrue(output.contains("some large string chunk here")) +``` + +If assertion fails, you'll only: + +``` +org.opentest4j.AssertionFailedError: +Expected :true +Actual :false +``` + +For AssertJ assertion: + +```java +Assertions.assertThat(output).contains("some large string chunk here"); +``` + +You'll have all required info in the console: + +``` +java.lang.AssertionError: +Expecting actual: + " + original text here + +" +to contain: + "some large string chunk here" +``` + +This is extremely helpful because often output is pre-processed with regexps +(to remove windows "\r", to replace varying part (e.g. times), etc.) and +AssertJ error shows the processed text which greatly simplifies understanding the problem. + +## Asserting collections + +```java +List hobbits = list(frodo, sam, pippin); + +// all elements must satisfy the given assertions +assertThat(hobbits).allSatisfy(character -> { + assertThat(character.getRace()).isEqualTo(HOBBIT); + assertThat(character.getName()).isNotEqualTo("Sauron"); +}); + +// at least one element must satisfy the given assertions +assertThat(hobbits).anySatisfy(character -> { + assertThat(character.getRace()).isEqualTo(HOBBIT); + assertThat(character.getName()).isEqualTo("Sam"); +}); +``` + +Accessing elements: + +```java +Iterable hobbits = list(frodo, sam, pippin); +assertThat(hobbits).first().isEqualTo(frodo); +assertThat(hobbits).element(1).isEqualTo(sam); +assertThat(hobbits).last().isEqualTo(pippin); +``` + +## Exception assertions + +```java +assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> { throw new RuntimeException(new IllegalArgumentException("boom!")); }) + .havingCause() + .withMessage("boom!"); +``` + +Or + +```java +assertThatThrownBy(() -> throw new RuntimeException("boom!")) + .hasMessage("boom!"); +``` + +## Objects comparison + +For data objects, not implementing equals simple comparison would not work: + +```java +Object first = new Something(); +Object second = new Something(); + +// different objects because equals not implemented +assertThat(first).isEqualTo(second); +``` + +AssertJ provides [recursive comparison](https://assertj.github.io/doc/#assertj-core-recursive-comparison) +to compare object fields (instead of using object equals): + +```java +assertThat(first) + .usingRecursiveComparison() + .isEqualTo(second); +``` + +You can also exclude some fields from comparison: + +```java +assertThat(first) + .usingRecursiveComparison() + .ignoringFields("birthdDate") + .isEqualTo(second); +``` + +!!! tip + Assert object does not contain null fields: + ```java + assertThat(object).usingRecursiveAssertion().hasNoNullFields() + ``` + + +## Assumptions + +[Assumption mechanism](https://assertj.github.io/doc/#assertj-core-assumptions) allows ignoring test if some +condition does not met: + +```java +@Test +public void when_an_assumption_is_not_met_the_test_is_ignored() { + // since this assumption is obviously false ... + Assumptions.assumeThat(frodo.getRace()).isEqualTo(ORC); + // ... this assertion is not performed + assertThat(fellowshipOfTheRing).contains(sauron); +} +``` + + +## Soft assertions + +[Soft assertions](https://assertj.github.io/doc/#assertj-core-soft-assertions) +allows showing all errors at once, instead of only the first one. + +This might be useful for speeding up debugging long-running tests (avoid many run-fix-run cycles): + +```java +SoftAssertions.assertSoftly(softly -> { + softly.assertThat(frodo.name).isEqualTo("Samwise"); + softly.assertThat(sam.name).isEqualTo("Frodo"); +}); +``` + +``` +Multiple Failures (2 failures) + -- failure 1 -- + Expecting: + <"Frodo"> + to be equal to: + <"Samwise"> + but was not. + -- failure 2 -- + Expecting: + <"Samwise"> + to be equal to: + <"Frodo"> + but was not. +``` + +## DB assertions + +There is also an [assertj-db](https://assertj.github.io/doc/#assertj-db) extension which greatly +simplifies testing logic affecting JDBC database. + +To use assertj-db add dependency: + +```groovy +testImplementation 'org.assertj:assertj-db:3.0.0' +``` + +Assuming database is configured in application configuration: + +```java + +AssertDbConnection connection; + +@BeforeAll +// here AppConfig would be injected as guice bean (assume junit extension used) +static void beforeAll(AppConfig config) { + final DataSourceFactory db = config.getDatabase(); + connection = AssertDbConnectionFactory + .of(db.getUrl(), db.getUser(), db.getPassword()) + .create(); +} +``` + +Now you can access any table: + +```java +Table table = connection.table("table_name").build(); +``` + +!!! important + `Table` represents current database "snapshot" - it will not show modifications + performed after table creation! So always create new table before assertions. + + +To output it to console (useful for modifications on empty or small tables): + +```java +Outputs.output(table).toConsole(); +``` + +Will print the entire table in console: + +``` +[MEMBERS table] +|-----------|---------|-----------|-----------|--------------|-----------|-----------|-----------| +| | | * | | | | | | +| | PRIMARY | ID | NAME | FIRSTNAME | SURNAME | BIRTHDATE | SIZE | +| | KEY | (NUMBER) | (TEXT) | (TEXT) | (TEXT) | (DATE) | (NUMBER) | +| | | Index : 0 | Index : 1 | Index : 2 | Index : 3 | Index : 4 | Index : 5 | +|-----------|---------|-----------|-----------|--------------|-----------|-----------|-----------| +| Index : 0 | 1 | 1 | Hewson | Paul David | Bono | 05-10-60 | 1.75 | +| Index : 1 | 2 | 2 | Evans | David Howell | The Edge | 08-08-61 | 1.77 | +| Index : 2 | 3 | 3 | Clayton | Adam | | 03-13-60 | 1.78 | +| Index : 4 | 4 | 4 | Mullen | Larry | | 10-31-61 | 1.70 | +|-----------|---------|-----------|-----------|--------------|-----------|-----------|-----------| +``` + +Assert [table data](https://assertj.github.io/doc/#assertj-db-concepts-table): + +```java +org.assertj.db.api.Assertions.assertThat(table).hasNumberOfRows(1); + +org.assertj.db.api.Assertions.assertThat(table).column("name") + .value().isEqualTo("Hewson") +``` + +Do direct [sql requests](https://assertj.github.io/doc/#assertj-db-concepts-request): + +```java +Request request1 = connection.request("select name, firstname from members where id = 2 or id = 3").build(); +``` + +!!! tip + Read more in [concepts doc](https://assertj.github.io/doc/#assertj-db-concepts) \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/client.md b/dropwizard-guicey/src/doc/docs/guide/test/general/client.md new file mode 100644 index 000000000..540dece1e --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/client.md @@ -0,0 +1,753 @@ +# Testing web (HTTP client) + +`ClientSupport` is a [JerseyClient](https://eclipse-ee4j.github.io/jersey.github.io/documentation/2.29.1/client.html) +aware of dropwizard configuration, so you can easily call admin/main/rest urls. + +Creation: + +```java +ClientSupport client = TestSupport.webClient(support); +``` + +where support is `DropwizardTestSupport` or `GuiceyTestSupport` (in later case it could be used only as generic client for calling external urls). + +!!! note + There are 4 base urls: + + * Server root: http://localhost:8080/ + * Application root: http://localhost:8080/app ("/" by default but could be changed: `server.applicationContextPath = 'app'`) + * Admin root: http://localhost:8081/ or http://localhost:8080/admin (simple server with `server.adminContextPath = 'admin'`) + * Rest root: http://localhost:8080/rest/ ("/" by default, but could be changed: `server.rootPath = 'rest'`) + + By default, all of them are "/" (root), but could be changed. + +`CientSupport` is a client for server root (everything after port). Usually, +a more specific client is required (app/admin/rest): + +```java +TestClient app = client.appClient(); +TestClient admin = client.adminClient(); +TestClient rest = client.restClient(); +``` + +Specific client usage guarantees url correctness in case of server configuration change. +For example, an integration test may use a random port and clients will use the correct one. + +!!! note + `ClientSupport` is also extends `TestClient`, so it is 4rth client (for application root) + and provides the same shortcut methods as other clients. + + Also, `ClientSupport` provide special shortcuts for jersey api: + + ```java + // GET {rest path}/some + client.targetRest("some").request().buildGet().invoke() + + // GET {main context path}/servlet + client.targetApp("servlet").request().buildGet().invoke() + + // GET {admin context path}/adminServlet + client.targetAdmin("adminServlet").request().buildGet().invoke() + + // General external url call + client.target("https://google.com").request().buildGet().invoke() + ``` + +If you want to construct a custom client, `ClientSupport` object can provide all required info: + +```groovy +client.getPort() // app port (8080) +client.getAdminPort() // app admin port (8081) +client.basePathRoot() // root server path (http://localhost:8080/) +client.basePathApp() // main context path (http://localhost:8080/) +client.basePathAdmin() // admin context path (http://localhost:8081/) +client.basePathRest() // rest context path (http://localhost:8080/) +``` + +## Simple shortcuts + +`TestClient` provides simple shortcuts for the GET/POST/PUT/PATCH/DELETE methods: + +```java +@Test +public void testWeb(ClientSupport client) { + TestClient rest = client.restClient(); + + // get with simple result + Result res = client.get("/sample", Result.class); + // get with simple result list + List res = client.get("/list", new GenericType<>() {}); + + // post without result (void) + client.post("/post", new PostObject()); + + // post with result + Result res = client.post("rest/action", new PostObject(), Result.class); +} +``` + +## String.format + +String format could be used for all methods: + +```java +client.targetRest("some/%s", 12).request().buildGet().invoke() + +client.targetRest("some/%s", 12).request().buildGet().invoke(String.class) + +client.get("/some/%s", User.class, 12) +``` + +## Defaults + +Each `TestClient` provide "default*" methods to set request defaults: + +* `defaultHeader("Name", "value")` +* `defaultQueryParam("Name", "value")` +* `defaultCookie("Name", "value")` +* `defaultAccept("application/json")` +* etc. + +The most obvious use case is authorization: + +```java +public void testSomething(ClientSupport client) { + client.defaultHeader("Authorization", "Bearer 123"); + + User user = client.restClient().get("/users/123", User.class); +} +``` + +Defaults could be cleared at any time with `client.reset()`. + +## Sub clients + +There is a concept of sub clients. It is used to create a client for a specific sub-url. +For example, suppose all called methods in test have some base path: `/{somehting}/path/to/resource`. +Instead of putting it into each request: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient(); + + rest.get("/%s/path/to/resource/%s", User.class, "path", 12); + rest.post("/%s/path/to/resource/%s", new User(...), "path", 12); +} +``` + +A sub client could be created: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient().subClient("/{something}/path/to/resource") + .defaultPathParam("something", "path"); + + rest.get("/%s", User.class, "path", 12); + rest.post("/%s", new User(...), "path", 12); +} +``` + +!!! note + Sub clients inherit defaults of parent client. + + ```java + client.defaultQueryParam("q", "v"); + TestClient rest = client.subClient("/path/to/resource"); + + // inherited query parameter q=v will be applied to all requests + rest.get("/%s", User.class, 12); + ``` + +There is a special sub client creation method using jersey `UriBuilder`, required +to properly support matrix parameters in the middle of the path: + +```java +TestClient sub = client.subClient(builder -> builder.path("/some/path").matrixParam("p", 1)); + +// /some/path;p=1/users/12 +sub.get("/users/%s", User.class, 12); +``` + +## Builder API + +Request builder API covers all possible configurations +for jersey `WebTarget` and `Invocation.Builder`. The main idea was to simplify +request configuration: to provide all possible methods in one place. + +For example: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .as(User.class) +``` + +Request specific extensions and properties are also supported: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .register(VoidBodyReader.class) + .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) + .asVoid(); +``` + +All builder methods start with a "build" prefix (`buildGet()`, `buildPost()` or generic `build()`). + +Builder provides direct value mappings: + +* `.as(Class)` +* `.as(GenericType)` +* `.asVoid()` +* `.asString()` + +And methods, returning raw (wrapped) response: + +* `.invoke()` - response without status checks +* `.expectSuccess()` - fail if not success +* `.expectSuccess(201, 204)` - fail if not success or not expected status +* `.expectRedirect()` - fail if not redirect (method also disabled redirects following) +* `.expectRedirect(301)` - fail if not redirect or not expected status +* `.expectFailure()` - fail if success +* `.expectFailure(400)` - fail success or not expected status + +## Debug + +Considering the client defaults inheritance (potential decentralized request configuration), +it might be unobvious what was applied to the request. + +Request builder provides a `debug()` option, which will print all applied defaults +and direct builder configurations to the console: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .debug() + .as(User.class) +``` + +``` +Request configuration: + + Path params: + p1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:61) + p2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + p3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + + Query params: + q1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:57) + q2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + q3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + + Accept: + application/json at r.v.d.g.t.c.builder.(RequestBuilderTest.java:54) + +Jersey request configuration: + + Resolve template at r.v.d.g.t.c.builder.(TestRequestConfig.java:869) + (encodeSlashInPath=false encoded=true) + p1=1 + p2=2 + p3=3 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q1=1 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q2=2 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q3=3 + + Accept at r.v.d.g.t.c.builder.(TestRequestConfig.java:899) + [application/json] +``` + +It shows two blocks: + +* How request builder was configured (including defaults source) +* How jersey request was configured + +The latter is obtained by wrapping jersey `WebTarget` and `Invocation.Builder` +objects to intercept all calls. + +Debug could be enabled for all requests: `client.defaultDebug(true)`. + +## Request assertions + +It would not be very useful for the majority of cases, but as debug api could +aggregate all request configuration data, it is possible to assert on it: + +```java +client.buildGet("/some/path") + .matrixParam("p1", "1") + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("/some/path;p1=1")) + .as(SomeEntity.class); +``` + +or + +```java +.assertRequest(tracker -> assertThat(tracker.getQueryParams().get("q")).isEqualTo("1")) +``` + +## Response assertions + +Request builder methods like `.invoke()` or `.expectSuccess()` returns +a special response wrapper object. It provides a lot of useful assertions to simplify +response data testing (avoid boilerplate code). + +For example, check a response header, cookie and obtain value + +```java +User user = rest.buildGet("/users/123") + .expectSuccess() + .assertHeader("Token" , s -> s.startsWith("My-Header;")) + .assertCookie("MyCookie", "12") + .as(User.class); +``` + +Here assertion error will be thrown if header or cookie was not provided or condition does not match. + +Even if you need to obtain a header or cookie value from response, you can use assetions to verify +header/cookie presence: + +```java +Response response = rest.buildGet("/users/123") + .expectSuccess() + .assertHeader("Token" , s -> s.startsWith("My-Header;")) + .asResponse(); + +// here you could be sure the header exists +String token = response.getHeaderString("Token"); +``` + +Redirection correctness could be checked as: + +```java +@Path("/resources") +public class Resource { + + @Inject + AppUrlBuilder urlBuilder; + + @Path("/list") + @GET + public Response get() { + ... + } + + @Path("/redirect") + @GET + public Response redirect() { + return Response.seeOther( + urlBuilder.rest(SuccFailRedirectResource.class).method(Resource::get).buildUri() + ).build(); + } +} +``` + +```java +rest.method(Resource::redirect) + // throw error if not 3xx; also, this disables redirects following + .expectRedirect() + .assertHeader("Location", s -> s.endsWith("/resources/list")); +``` + +Also, "with*" methods could be used for completely manual assertions: + +```java +rest.method(Resource::redirect) + .expectSuccess(201) + .withHeader("MyHeader", s -> + assertThat(s).startsWith("My-Header;")); +``` + +Response object could be converted without additional variables: + +```java +String value = rest.method(Resource::redirect) + .expectSuccess() + .as(res -> res.readEntity(SomeClass.class).getProperty()); +``` + +## Form builder + +There is a special builder helping build urlencoded and multipart requests (forms): + +```java +// urlencoded +client.buildForm("/some/path") + .param("name", 1) + .param("date", 2) + .buildPost() + .as(String.class); + +// multipart +client.buildForm("/some/path") + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildPost() + .asVoid(); +``` + +!!! tip + Compare with raw jersey api usage: + + ```java + FormDataMultiPart multiPart = new FormDataMultiPart(); + multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + + FileDataBodyPart fileDataBodyPart = new FileDataBodyPart("file", + file.toFile(), + MediaType.APPLICATION_OCTET_STREAM_TYPE); + multiPart.bodyPart(fileDataBodyPart); + + rest.post(path, Entity.entity(multiPart, multiPart.getMediaType()), Something.class); + ``` + +Also, it could be used to simply create a request entity and use it directly: + +```java +Entity entity = client.buildForm(null) + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildEntity() + +client.post("/some/path", entity); +``` + +Builder will serialize all provided (non-multipart) parameters to string. +For dates, it is possible to specify a custom date format: + +```java +client.buildForm("/some/path") + .dateFormat("dd/MM/yyyy") + .param("date", new Date()) + .param("date2", LocalDate.now()) + .buildPost() + .asVoid(); +``` + +(java.util and java.time date formatters could be set separately with `dateFormatter()` or `dateTimeFormatter()` methods) + +The default format could be changed globally: `client.defaultFormDateFormat("dd/MM/yyyy")` +(or `defaultFormDateFormatter()` with `defaultFormDateTimeFormatter()`). + +## Jersey API + +It is possible to use `client.target("/path")` to build raw jersey target +(with the correct base path). But without applied defaults. + +Direct `Invocation.Builder` could be built with `client.request("/path")`. +Here all defaults would be applied. + +Builder API does not hide native jersey API: + +* `WebTarget` - could be modified directly with `request.configurePath(target -> target.path("foo"))` +* `Invocation.Builder` - with `request.configureRequest(req -> req.header("foo", "bar"))` + +Such modifiers could be applied as client defaults: + +* `client.defaultPathConfiguration(...)` +* `client.defaultRequestConfiguration(...)` + +Response wrapper also provides direct access to jersey `Response` object: +`response.asResponse()`. + +## Resource clients + +There is a special type of type-safe clients based on the simple idea: +resource class declaration already provides all required metadata to configure a test request: + +```java +@Path("/users") +public class UserResource { + + @Path("/{id}") + @GET + public User get(@NotNull @PathParam("id") Integer id) {} +} +``` + +Resource declares its path in the root `@Path` annotation and method annotations +tell that it's a GET request on path `/users/{id}` with required path parameter. + +```java +// essentially, it's a sub client build with the resource path (from @Path annotation) +ResourceClient rest = client.restClient(UserResource.class); + +User user = rest.method(r -> r.get(123)).as(User.class); +``` + +By using a mock object call (`r -> r.get(123)`) we specify a source of metadata and the required values +for request. Using it, a request builder is configured automatically. + +It is not required to use all parameters (reverse mapping is not always possible): +use null for not important arguments. All additional configurations could be done manually: + +```java +ResourceClient rest = client.restClient(UserResource.class); + +User user = rest.method(r -> r.get(null)) + .pathParam("id", 123) + .as(User.class); +``` + +Almost everything could be recognized: + +* All parameter annotations like `@QueryParam`, `@PathParam`, `@HeaderParam`, `@MatrixParam`, `@FormParam`, etc. +* All request methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. +* Request body mapping: `void post(MyEntity entity)` +* And even multipart forms + +Not related arguments should be simply ignored: + +```java +public void get(@PathParam("id") Integer id, @Context HttpServletRequest request) {} + +rest.method(r -> r.get(123, null)); +``` + +!!! note + `ResourceClient` extends `TestClient`, so all usual method shortcuts are also available for resource client + (real method calls usage is not mandatory). + +### Multipart forms + +Multipart resource methods often use special multipart-related entities, like: + +```java + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) +``` + +Which is not handy to create manually. To address this, `ResourceClient` provides a +special helper object to build multipart-related values: + +```java +rest.multipartMethod((r, multipart) -> + r.multipart(multipart.fromClasspath("/sample.txt"), + multipart.disposition("file", "sample.txt")) + .asVoid()); +``` + +Here file stream passed as a first parameter and filename with the second one. + +Or + +```java + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2( + @NotNull @FormDataParam("file") FormDataBodyPart file) +``` + +```java + rest.multipartMethod((r, multipart) -> + r.multipart2(multipart.streamPart("file", "/sample.txt"))) + .asVoid(); +``` + +In case of generic multipart object argument: + +```java + @Path("/multipartGeneric") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartGeneric(@NotNull FormDataMultiPart multiPart) +``` + +there is a special builder: + +```java +rest.multipartMethod((r, multipart) -> + r.multipartGeneric(multipart.multipart() + .field("foo", "bar") + .stream("file", "/sample.txt") + .build())) + .as(String.class); +``` + +!!! note + Multipart methods require the urlencoded client (default) and, most likely, + will fail with the apache client. + +### Sub resources + +When a sub resource is declared with an instance: + +```java +public class Resource { + @Path("/sub") + public SubResource sub() { + return new SubResource(); + } +} +``` + +it could be easily called directly: + +```java +User user = rest.method(r -> r.sub().get(123)).as(User.class); +``` + +When a sub resource method is using class: + +```java +public class Resource { + @Path("/sub") + public Class sub() { + return SubResource.class; + } +} +``` + +you'll have to build a sub-client first: + +```java +ResourceClient subRest = rest.subResource(Resource::sub, SubResource.class); +``` + +!!! important + Jersey ignores sub-resource `@Path` annotation, so special method for sub resource clients is required. + +### Resource typification + +It is not always possible to use resource class to buld a sub client +(with `.restClient(Resource.class)`). + +In such cases you can build a resource path manually and then "cast" client to the resource type: + +```java +ResourceClient rest = client.subClient("/resource/path") + .asResourceClient(MyResource.class); +``` + +or just build path manually: + +```java +ResourceClient rest = client.subClient( + builder -> builder.path("/resource").matrixParam("p", 123), + MyResource.class); +``` + + +## Apache client + +By default, the client is based on "url connector", which has a limitation for PATCH +requests: on java > 16 PATCH requests will not work without additional `--add-opens`. +For such requests it is easier to use an apache connector. + +It is not possible to use apache connector by default because it +[has problems](https://github.com/eclipse-ee4j/jersey/issues/5528#issuecomment-1934766714) +with multipart requests). + +You can switch connector type either by providing different `TestClientFactory` +or by calling `ClientSupport` shortcuts: + +* `client.apacheClient()` - `ClientSupport` with apache connector +* `client.urlconnectorClient()` - `ClientSupport` with url connector + +With these shortcuts you can use both connectors in the same test. + +## Customization + +`JerseyClient` used in `ClientSupport` could be customized using `TestClientFactory` implementation. + +Simple factory example: + +```java +public class SimpleTestClientFactory implements TestClientFactory { + + @Override + public JerseyClient create(final DropwizardTestSupport support) { + return new JerseyClientBuilder() + .register(new JacksonFeature(support.getEnvironment().getObjectMapper())) + .property(ClientProperties.CONNECT_TIMEOUT, 1000) + .property(ClientProperties.READ_TIMEOUT, 5000) + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) + .build(); + } +} +``` + +or using the default implementation as base: + +```java +public class SimpleTestClientFactory extends DefaultTestClientFactory { + + @Override + protected void configure(final JerseyClientBuilder builder, final DropwizardTestSupport support) { + builder.getConfiguration().connectorProvider(new Apache5ConnectorProvider()); + } +} +``` + +Default implementation (`DefaultTestClientFactory`) applies timeouts and auto-registers multipart support if `dropwizard-forms` module +if available in classpath. + +All builders support `.clientFactory()` method for optional customization. + + +## Default client + +`JerseyClient` used inside `ClientSupport` is created by `DefaultTestClientFactory`. + +Default implementation: + +1. Enables multipart feature if `dropwizard-forms` is in classpath (so the client could be used + for sending multipart data). +2. Enables request and response logging to simplify writing (and debugging) tests. + +By default, all request and response messages are written directly into console to guarantee client +actions visibility (logging might not be configured in tests). + +Example output: + +``` + +[Client action]---------------------------------------------{ +1 * Sending client request on thread main +1 > GET http://localhost:8080/sample/get + +}---------------------------------------------------------- + + +[Client action]---------------------------------------------{ +1 * Client response received on thread main +1 < 200 +1 < Content-Length: 13 +1 < Content-Type: application/json +1 < Date: Mon, 27 Nov 2023 10:00:40 GMT +1 < Vary: Accept-Encoding +{"foo":"get"} + +}---------------------------------------------------------- +``` + +Console output might be disabled with a system proprty: + +```java +// shortcut sets DefaultTestClientFactory.USE_LOGGER property +DefaultTestClientFactory.disableConsoleLog() +``` + +With it, everything would be logged into `ClientSupport` logger (java.util) under INFO +(most likely, would be invisible in the most logger configurations, but could be enabled). + + +To reset property (and get logs back into console) use: + +```java +DefaultTestClientFactory.enableConsoleLog() +``` + +!!! note + Static methods added not directly into `ClientSupport` because this is + the default client factory feature. You might use a completely different factory. diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/command.md b/dropwizard-guicey/src/doc/docs/guide/test/general/command.md new file mode 100644 index 000000000..85eb2d444 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/command.md @@ -0,0 +1,122 @@ +# Testing commands + +`CommandTestSupport` object is a commands test utility equivalent to `DropwizardTestSupport`. +It uses dropwizard `Cli` for arguments recognition and command selection. + +The main difference with `DropwizardTestSupport` is that command execution is +a short-lived process and all assertions are possible only *after* the execution. +That's why command runner would include in the result all possible dropwizard objects, +created during execution (because it would be impossible to reference them after execution). + +New builder (almost the same as application execution builder) simplify commands execution: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .run("simple", "-u", "user") + +Assertions.assertTrue(result.isSuccessful()); +``` + +This runner could be used to run *any* command type (simple, configured, environment). +The type of command would define what objects would be present ofter the command execution +(for example, `Injector` would be available only for `EnvironmentCommand`). + +Run command arguments are the same as real command arguments (the same `Cli` used for commands parsing). +You can only omit configuration path and use builder instead: + +```java + CommandResult result = TestSupport.buildCommandRunner(App.class) + .config("path/to/config.yml") + .configOverride("prop: 1") + .run("cmd", "-p", "param"); +``` + +!!! important + Such run *never fails* with an exception: any appeared exception would be + stored inside the response: + + ```java + Assertions.assertFalse(result.isSuccessful()); + Assertions.assertEquals("Error message", result.getException().getMessage()); + ``` + +### IO + +Runner use System.in/err/out replacement. All output is intercepted and could be +asserted: + +```java +Assertions.assertTrue(result.getOutput().contains("some text")) +``` + +`result.getOutput()` contains both `out` and `err` streams together +(the same way as user would see it in console). Error output is also available +separately with `result.getErrorOutput()`. + +!!! note + All output is always printed to console, so you could always see it after test execution + (without additional actions) + +Commands requiring user input could also be tested (with mocked input): + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .consoleInputs("1", "two", "something else") + .run("quiz") +``` + +At least, the required number of answers must be provided (otherwise error would be thrown, +indicating not enough inputs) + +!!! warning + Due to IO overrides, command tests could not run in parallel. + For junit 5, such tests could be annotated with [`@Isolated`](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization) + (to prevent execution in parallel with other tests) + +### Configuration + +Configuration options are the same as in run builder. For example: + +```java +// override only +TestSupport.buildCommandRunner(App.class) + .configOverride("foo: 12") + .run("cfg"); + +// file with overrides +TestSupport.buildCommandRunner(App.class) + .config("src/test/resources/path/to/config.yml") + .configOverride("foo: 12") + .run("cfg"); + +// direct config object +MyConfig config = new MyConfig(); +TestSupport.buildCommandRunner(App.class) + .config(config) + .run("cfg"); +``` + +!!! note + Config file should not be specified in command itself - builder would add it, if required. + But still, it would not be a mistake to use config file directly in command: + + ```java + TestSupport.buildCommandRunner(App.class) + // note .config("...") was not used (otherwise two files would appear)! + .run("cfg", "path/to/config.yml"); + ``` + + Using builder for config file configuration assumed to be a preferred way. + +### Listener + +There is a simple listener support (like in application run builder) for setup-cleanup actions: + +```java +TestSupport.buildCommandRunner(App.class) + .listen(new CommandRunBuilder.CommandListener<>() { + public void setup(String[] args) { ... } + public void cleanup(CommandResult result) { ... } + }) + .run("cmd") +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/general.md b/dropwizard-guicey/src/doc/docs/guide/test/general/general.md new file mode 100644 index 000000000..6cdc3ac7f --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/general.md @@ -0,0 +1,33 @@ +# General test tools + +!!! note "Junit 5" + If you're going to use junit 5, go straight to [junit 5 section](../junit5/setup.md): + all required general tools usage scenarios are described there. + +Test framework-agnostic tools. +Useful when: + + - There are no extensions for your test framework + - Assertions must be performed after test app shutdown (or before startup) + - Commands testing + +Test utils: + + - `TestSupport` - root utilities class, providing easy access to other helpers + - `DropwizardTestSupport` - [dropwizard native support](https://www.dropwizard.io/en/release-4.0.x/manual/testing.html#non-junit) for full integration tests + - `GuiceyTestSupport` - guice context-only integration tests (without starting web part) + - `CommandTestSupport` - general commands tests + - `ClientSupport` - web client helper (useful for calling application urls) + +!!! important + `TestSupport` assumed to be used as a universal shortcut: everything could be created/executed through it + so just type `TestSupport.` and look available methods - *no need to remember other classes*. + +Additional features implemented with hooks: + +- [StubsHook](stubs.md) - stubs support +- [MocksHook](mocks.md) - mocks support +- [SpiesHook](spies.md) - spies support +- [RestStubsHook](rest.md) - lightweight REST testing +- [RecordLogsHook](logs.md) - logs testing +- [TrackersHook](tracks.md) - guice bean calls recording and performance testing \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/logs.md b/dropwizard-guicey/src/doc/docs/guide/test/general/logs.md new file mode 100644 index 000000000..26ec2a6fb --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/logs.md @@ -0,0 +1,185 @@ +# Testing logs + +Guicey provide `RecordLogsHook` for capturing logged messages. + +!!! important + Works only with logback (default dropwizard logger). + +For example, suppose some service logs some technical hint at some condition: + +```java +public class Service { + private final Logger logger = LoggerFactory.getLogger(Service.class); + + public void foo() { + ... + if (someCondition) { + logger.info("Some technical note"); + } + } +} +``` + +Testing that technical note actually logged: + +```java +public class LogsTest { + + @Test + public void testLog() { + RecordedLogHook hook = new RecordedLogHook(); + // start recorder registration + RecordedLogs logs = hook.record() + // listen only for one logger + .logger(Service.class) + // start recording all logger messages of level INFO and above + .start(Level.INFO); + + // run application with hook + TestsSupport.build(MyApp.class) + .hooks(hook) + .runCore(injector -> { + // run method foo (assume log message must appear) + injector.getInstance(Service.class).foo(); + }); + + // one log recorded + Assertions.assertEquals(1, logs.count()); + // log message appears + Assertions.assertEquals(1, logs.containing("Some technical note").count()); + // alternative: last message was a technical hint + Assertions.assertEquals("Some technical note", logs.lastMessage()); + } +} +``` + +!!! warning + Such tests could not be run in parallel because logger configuration is global + +## Registration + +You can register as many recorders as you like. Each recorder could listen one or more +loggers. + +To listen all warnings (root logger): + +```java +hook.register().start(Level.WARN); +``` + +To listen all loggers in package: + +```java +hook.register().loggers("com.my.package").start(Level.WARN); +``` + +To listen exact class and package: + +```java +hook.register() + .loggers(SomeClass.class) + .loggers("com.my.package") + .start(Level.INFO); +``` + +## Implementation details + +Each recorder registration leads to logging appender registration for a target logger +(or multiple loggers). + +If the current logger configuration is higher than required, then **logger would be +re-configured**. For example, if default logger level is `INFO` and recorder requires `TRACE` messages, +then it would change logger configuration to receive required messages. + +!!! tip + Recorder might be used just to enable required logs, without application + logging configuration. This is very useful in tests (to enable `DEBUG` or `TRACE` + messages for exact service (or package)): `hook.register().loggers(MyClass.class).start(Level.TRACE)` + +During application startup **dropwizard resets loggers two times** and hook would +re-attach appenders to compensate it. You should be able to record all messages from application startup, +except logs from dropwizard bundles, registered BEFORE `GuiceBundle`. + +If required, actual recorder object is accessible with `RecordedLog#getRecorder()`: +it provides `attach()` and `destroy()` methods (for attaching and detaching appender). +The hook would call these methods automatically. + +## Querying + +`RecordedLogs` used to query recorded logs. Root object always contains all recorded events +(for configured loggers). + +Recorded logs are accessible in form of raw *event* (`ILoggingEvent`) or pure string *message* +(formatted messages with arguments). + +| Method | Description | Example | +|-------------------|-------------------------------------------------|---------------------------------------------------------------------------| +| `count()` | Recorded logs count | `assertEquals(1, logs.count())` | +| `empty()` | Events recorded | `assertFalse(logs.empty())` | +| `events()` | All recorded events | `List events = logs.events()` | +| `messages()` | Messages of all recorded messages | `List messages = logs.messages()` | +| `has(loggerName)` | Checks if messages from target logger available | `assertTrue(logs.has(Service.class))`, `assertTrue(logs.has("com.some"))` | +| `has(level)` | Checks if messages of level available | `assertTrue(logs.has(Level.WARN))` | +| `lastEvent()` | Last recorded event or null | `assertEquals(Level.WARN, logs.lastEvent().getLevel())` | +| `lastMessage()` | Message of the last recorded event or null | `assertEquals("Something", logs.lastMessage())` | + +Also, logs could be filtered: + +| Filter | Description | Example | +|----------------------|------------------------------------------------|--------------------------------------------------------------| +| `level(level)` | Select events with level | `logs.level(Level.WARN)` | +| `logger(loggerName)` | Select events of required loggers | `logs.logger(Service.class)`, `logs.logger("com.some")` | +| `containing(String)` | Events where messages contains provided string | `logs.containing("Substring")` | +| `matching(regex)` | Events where messages match provided regex | `logs.matching("something \\d+")` | +| `select(predicate)` | General events matching predicate | `logs.select(event -> event.getLevel().equals(Level.TRACE))` | + +Filters return another matcher object where all verification and filter methods above could be called +(multiple filters could be applied consequently). + +For example, verify count of all messages containing string: + +```java +assertEquals(1, logs.containing("Something").count()); +``` + +Or filtering by logger and level (if recorder records multiple loggers): + +```java +assertEquals(12, logs.logger("com.some.package").level(Level.WARN).count()) +``` + +## Clear recordings + +Recorded logs could be cleared at any time (to simplify exact method logs matching): + +```java +// clear logs, recorded during application startup +logs.clear(); +// call method +service.foo(); +// verify logs appeared during method call +assertEquals(1, logs.containing("Something").count()); + +// clear again to check logs of another method +logs.clear(); +service.boo(); +... +``` + +## Hook methods + +Logs from all recorders could be cleared with hook: + +```java +hook.clearLogs() +``` + +To detach all registered appenders: + +```java +hook.destroy() +``` + +!!! note + Dropwizard resets loggers during startup so manual detach should not be required + (to avoid keeping stale appenders between tests). \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/mocks.md b/dropwizard-guicey/src/doc/docs/guide/test/general/mocks.md new file mode 100644 index 000000000..a403fbc89 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/mocks.md @@ -0,0 +1,218 @@ +# Testing with mocks + +[Mockito](https://site.mockito.org/) mocks are essentially an automatic [stubs](stubs.md): +with the ability to dynamically declare method behavior (by default, all mock methods +return default value: often null). + +Guicey provides `MocksHook` for overriding guice beans with mockito mocks. + +!!! warning + Stubs will not work for HK2 beans + +Mockito documentation is written in the `Mockito` class [javadoc](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html). +Additional docs could be found in [mockito wiki](https://github.com/mockito/mockito/wiki/FAQ) +Also, see official [mockito refcard](https://dzone.com/refcardz/mockito) +and [baeldung guides](https://www.baeldung.com/mockito-series). + +## Setup + +Requires mockito dependency (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +!!! note "Remember" + * Do not mock types you don’t own + * Don’t mock value objects + * Don’t mock everything + * Show love with your tests! + + [source](https://site.mockito.org/#more), [explanations](https://github.com/mockito/mockito/wiki/How-to-write-good-tests) + +For example, suppose we have a service: + +```java +public class Service { + public String foo() { + ... + } +} +``` + +where method foo implements some complex logic, not required in test. + +To override service with a mock: + +```java +MocksHook hook = new MocksHook(); +// register mock (mock would be created automatically using Mockito.mock(Service.class) +Service mock = hook.mock(Service.class); +// define method result +when(mock.foo()).thenReturn("static value"); + +TestsSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + Service service = injector.getInstance(Service.class); + + // mock instance instead of service + Assertions.assertEquals(mock, service); + // method overridden + Assertions.assertEquals("static value", service.foo()); + }); +``` + +Here `when` refer to `Mockito.when()` used with static import. + +!!! important + Guice AOP would not be applied to mocks (only guice-managed beans support AOP) + + +You can also provide a pre-created mock instance (this does not make much sence, but possible): + +```java +hook.mock(Service.class, mockInstance); +``` + +## Mocking examples + +Mocking answers for different arguments: + +```java +when(mock.foo(10)).thenReturn(100); +when(mock.foo(20)).thenReturn(200); +when(mock.foo(30)).thenReturn(300); +``` + +Different method answers (for consequent calls): + +```java +when(mock.foo(anyInt())).thenReturn(10, 20, 30); +``` + +Using actual argument in mock: + +```java + when(mock.getValue(anyInt())).thenAnswer(invocation -> { + int argument = (int) invocation.getArguments()[0]; + int result; + switch (argument) { + case 10: + result = 100; + break; + case 20: + result = 200; + break; + case 30: + result = 300; + break; + default: + result = 0; + } + return result; + }); +``` + +## Asserting calls + +Mock could also be used for calls verification: + +```java +// method Service.foo() called on mock just once +verify(mock, times(1)).foo(); +// method Service.bar(12) called just once (with exact argument value) +verify(mock, times(1)).bar(12); +``` + +These assertions would fail if method was called more times or using different arguments. + +## Mock reset + +If you run multiple tests with the same application, then it makes sense to re-configure +mocks for each test and so the previous mock state must be reset. + +Use `hook.resetMocks()` to reset all registered mocks + +## Partial mocks + +If mock is applied for a class with implemented methods, these methods would +still be overridden with fake implementations. If you want to preserve this logic, then +use spies: + +```java +public class AbstractService implements IService { + public abstract String bar(); + + public String foo() { + return "value"; + } +} + +AbstractService mock = Mockito.spy(AbstractService.class); +hook.mock(IService.class, mock); + + +IService service = injector.getInstance(IService.class); +// default mock implementation for abstract method +Assertions.assertNull(service.bar()); +// implemented method preserved +Assertions.assertEquals("value", service.foo()); +``` + +!!! note + The [spies](spies.md) section covers only spies, spying on real guice bean instance. + Using spies for partial mocks is more related to pure mocking and so it's described here. + +## Accessing mock + +Mock instance (used to configure methods behavior) could be obtained: + +1. On registration (`Service mock = hook.mock(Service.class)`) +2. From guice injector: `Service mock = injector.getInstance(Service.class)` + (as hook is registered by instance, guice AOP could not be applied for it and so it + would always be a raw mock) +3. From hook: `Service mock = hook.getMock(Service.class)` + +## Mocking OpenAPI client + +If you use some external API with a client, generated from openapi (swagger) declaration, +then you should be using it in code like this: + +```java + +@Inject +SomeApi api; + +public void foo() { + Some response = api.someGetCall(...) +} +``` + +Where `SomeApi` is a generated client class. + +Usually, the simplest way is to record real service response (using swagger UI or other generated documentation) +or simply enabling client debug in the application (so all requests and responses would be logged). + +Store such responses as json files in test resources: e.g. `src/test/resources/responses/someGet.json` + +Now mocking `SomeApi` and configure it to return object, mapped from json file content, instead of the real call: + +```java +MocksHook hook = new MocksHook(); +Service mock = hook.mock(SomeApi.class); +ObjectMapper mapper = new ObjectMapper(); +// define method result +when(mock.someGetCall(...)).thenReturn(mapper.readValue( + new File("src/test/resources/responses/someGet.json"), Some.class)); +``` + +With it, object, mapped from json file, would be returned on service call, instead of +the real api. + +!!! note + In the example, direct file access used instead of classpath lookup because + IDEA by default does not copy `.json` resources (it must be additionally configured) and + so direct file access is more universal. \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/output.md b/dropwizard-guicey/src/doc/docs/guide/test/general/output.md new file mode 100644 index 000000000..0e6585f47 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/output.md @@ -0,0 +1,29 @@ +# Testing console output + +There is a utility to capture console output: + +```java +String out = TestSupport.captureOutput(() -> { + + // run application inside + TestSupport.runWebApp(App.class, injector -> { + ClientSupport client = TestSupport.getContextClient(); + + // call application api endpoint + client.get("sample/get", null); + + return null; + }); +}); + +// uses assert4j, test that client was called (just an example) +Assertions.assertThat(out) + .contains("[Client action]---------------------------------------------{"); +``` + +Returned output contains both `System.out` and `System.err` - same as it would be seen in console. + +All output is also printed into console to simplify visual validation + +!!! warning + Such tests could not be run in parallel (due to system io overrides) diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/rest.md b/dropwizard-guicey/src/doc/docs/guide/test/general/rest.md new file mode 100644 index 000000000..5045f949e --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/rest.md @@ -0,0 +1,264 @@ +# Testing REST + +Guicey provides lightweight REST testing support: same as [dropwizard resource testing support](https://www.dropwizard.io/en/stable/manual/testing.html#testing-resources), +but with guicey-specific features. + +Such tests would not start web container: all rest calls are simulated (but still, it tests every part of resource execution). + +!!! important + Rest stubs work only with lightweight guicey run (they are simply useless when web container started) + +Lightweight REST could be started with `RestStubsRunner` hook: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .disableDropwizardExceptionMappers(true) + .build(); + +TestSupport.build(App.class) + .hooks(restHook) + .runCore(injector -> { + // pre-configured client to call resources with relative paths + RestClient rest = restHook.getRestClient(); + + String res = rest.get("/foo", String.class); + Assertions.assertEquals("something", res); + + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/error", String.class)); + Assertions.assertEquals("error message", ex.getResponse().readEntity(String.class)); + }); +``` + +!!! note + Extension naming is not quite correct: it is not a stub, but real application resources are used. + The word "stub" used to highlight the fact of incomplete startup: only rest without web. + +By default, all declared resources would be started with all existing jersey extensions +(filters, exception mappers, etc.). **Servlets and http filters are not started** +(guicey disables all web extensions to avoid their (confusing) appearance in console) + +## Selecting resources + +Real tests usually require just one resource (to be tested): + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .resources(MyResource.class) + .build(); +``` + +This way only one resource would be started (and all resources directly registered in +application, not as guicey extension). All jersey extensions will remain. + +Or a couple of resources: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .resources(MyResource.class, MyResource2.class) + .build(); +``` + +Or you may disable some resources: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .disableResources(MyResource2.class, MyResource3.class) + .build(); +``` + +## Disabling jersey extensions + +Often jersey extensions, required for the final application, make complications for testing. + +For example, exception mapper: dropwizard register default exception mapper which +returns only the error message, instead of actual exception (and so sometimes we can't check the real cause). + +`.disableDropwizardExceptionMappers(true)` disables extensions, registered by dropwizard. + +When default exception mapper enabled, resource throwing runtime error would return just error code: + +```java +@Path("/some/") +@Produces("application/json") +public class ErrorResource { + + @GET + @Path("/error") + public String get() { + throw new IllegalStateException("error"); + } +} +``` + +```java +WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/some/error", String.class)); + +// exception hidden, only generic error code +Assertions.assertTrue(ex.getResponse().readEntity(String.class) + .startsWith("{\"code\":500,\"message\":\"There was an error processing your request. It has been logged")); + +``` + +Without dropwizard exception mapper, we can verify exact exception: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .disableDropwizardExceptionMappers(true) + .build(); + +... + +ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.get("/error", String.class)); +// exception available +Assertions.assertTrue(ex.getCause() instanceof IllegalStateException); + +``` + +It might be useful to disable application extensions also with `.disableAllJerseyExtensions(true)`: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .disableDropwizardExceptionMappers(true) + .disableAllJerseyExtensions(true) + .build(); +``` + +This way raw resource would be called without any additional logic. + +!!! note + Only extensions, managed by guicey could be disabled: extensions directly registered + in dropwizard would remain. + +Also, you can select exact extensions to use (e.g., to test it): + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .jerseyExtensions(CustomExceptionMapper.class) + .build(); +``` + +Or disable only some extensions (for example, disabling extension implementing security): + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .disableJerseyExtensions(CustomSecurityFilter.class) + .build(); +``` + +## Requests logging + +By default, rest client would log requests and responses, + +```java +String res = rest.get("/foo", String.class); +Assertions.assertEquals("something", res); +``` + +``` +[Client action]---------------------------------------------{ +1 * Sending client request on thread main +1 > GET http://localhost:0/foo + +}---------------------------------------------------------- + + +[Client action]---------------------------------------------{ +1 * Client response received on thread main +1 < 200 +1 < Content-Length: 3 +1 < Content-Type: application/json +something + +}---------------------------------------------------------- +``` + +Logging could be disabled with `.logRequests(false)`: + +```java +final RestStubsRunner restHook = RestStubsRunner.builder() + .logRequests(false) + .build(); +``` + +## Container + +By default, [InMemoryTestContainerFactory](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/test-framework.html#d0e18552) +used. + + In-Memory container is not a real container. It starts Jersey application and + directly calls internal APIs to handle request created by client provided by + test framework. There is no network communication involved. This containers + does not support servlet and other container dependent features, but it is a + perfect choice for simple unit tests. + +If it is not enough (in-memory container does not support all functions), then +use `GrizzlyTestContainerFactory` + + The GrizzlyTestContainerFactory creates a container that can run as a light-weight, + plain HTTP container. Almost all Jersey tests are using Grizzly HTTP test container + factory. + +To activate grizzly container add dependency (version managed by dropwizard BOM): + +```groovy +testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2' +``` + +By default, grizzly would be used if it's available on classpath, otherwise in-memory used. +If you need to force any container type use: + +```java +// use in-memory container, even if grizly available in classpath +// (use to force more lightweight container, even if some tests require grizzly) +.container(TestContainerPolicy.IN_MEMORY) +``` + +```java +// throw error if grizzly container not available in classpath +// (use to avoid accidental in-memory use) +.container(TestContainerPolicy.GRIZZLY) +``` + +## Rest client + +`RestClient` is the same as [ClientSupport#restClient()](client.md), available for guicey extensions. +It extends the same `TestClient` class and so provides the same abilities: + +* [Defaults](client.md#defaults) +* [Shortcut methods](client.md#simple-shortcuts) +* [Builder API](client.md#builder-api) +* [Response assertions](client.md#response-assertions) +* [Static resource client](client.md#resource-clients) +* [Forms builder](client.md#form-builder) + +!!! note + Just in case: `ClientSupport` would not work with rest stubs (because web container is actually + not started and so `ClientSupport` can't recognize a correct rest mapping path). Of course, + it could be used with a full URLs. + +!!! note + Multipart support is enabled automatically when dropwizard-forms available in classpath. + Creating multipart request with [form bulder](client.md#form-builder): + + ```java + client.buildForm("/some/path") + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildPost() + .asVoid(); + ``` + +To clear defaults: + +```java +rest.reset() +``` + +Might be a part of call chain: + +```java +rest.reset().post(...) +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/run.md b/dropwizard-guicey/src/doc/docs/guide/test/general/run.md new file mode 100644 index 000000000..49181d552 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/run.md @@ -0,0 +1,326 @@ +# Testing application + +Application could be started in 2 ways: + +1. Core - create only guice injector (without starting web services) - ideal for business logic testing (pretty fast) +2. Web - full application start to test web endpoints (and complete flows) + +The second case is handled by [DropwizardTestSupport](https://www.dropwizard.io/en/release-4.0.x/manual/testing.html#non-junit) +and the first one by `GuiceyTestSupport` object (extending `DropwizardTestSupport`). + +There is a generic builder to simplify work with these objects (provides all possible configuration options): + +```java +RunResult result = TestSupport.build(App.class) + .config("src/test/resources/path/to/test/config.yml") + .configOverrides("foo: 2", "bar: 12") + .hooks(new MyHook()) + // run lightweight application (without web services) + .runCore() +``` + +!!! tip + `RunResult` contains both `DropwizardTestSupport` used for execution and `Injector` instance. + In other words, everything required for performing assertions. + +or with action: + +```java +Object serviceValue = TestSupport.build(App.class) + .config("src/test/resources/path/to/test/config.yml") + .configOverrides("foo: 2", "bar: 12") + .hooks(new MyHook()) + // run full application + .runWeb(injector -> { + return injector.getInstance(FooService.class).getSomething(); + }) +``` + +!!! note + Builder methods are almost equal to [junit 5 extension builder](../junit5/run.md#alternative-declaration) + +!!! important + All run methods declared as `throws Exception`. This was done to bypass original + exceptions instead of wrapping them inside runtime exceptions. + + This should not be a problem: just add `throws Exception` into test method signature + +## Configuration + +Configuration could be applied in a different ways: + +```java +// with override values only +TestSupport.build(App.class) + .configOverride("foo: 12") + +// file with overrides +TestSupport.build(App.class) + .config("src/test/resources/path/to/config.yml") + .configOverride("foo: 12") + +// file with config modifier +TestSupport.build(App.class) + .config("src/test/resources/path/to/config.yml") + .configModifiers(config -> config.setFoo(12)) + +// direct config object +MyConfig config = new MyConfig(); +TestSupport.build(App.class) + .config(config) +``` + +!!! note + Config override (`.configOverride()`) mechanism is provided by dropwizard: values are stored + as system properties and applied in time of configuration parsing. It might not work for some + properties (like collections) and it can't be used with manual configuration (last example). + + Config modifier (`.configModifiers()`) is a guicey concept: it could be used with either + configuration file or manual configuration object. It was added to simplify config modifications + and overcome limitations of config override. The only downside: when configuration is parsed + from file, modifier called after logging configuration, so to modify logging only config overrides + could be used. + + +Also, configuration source provider could be modified: + +```java +TestSupport.build(App.class) + .config("path/in/classpath/config.yml") + .configSourceProvider(new ResourceConfigurationSourceProvider()) +``` + +There are also configuration shortcuts: + +```java +TestSupport.build(App.class) + .randomPorts() + .restMapping("api") +``` + +To randomize used application ports (overrides config file values) and apply a different rest mapping path. + +When config overrides are used, they are always stored as system properties. This could be a problem +for parallel tests execution. To overcome this, test-unique prefixes could be used: + +```java +TestSupport.build(App.class) + .configOverride("foo", "1") + .propertyPrefix("something") +``` + +Junit 5 extensions use test class (and sometimes method) name to generate unique prefixes. + +## Lifecycle listeners + +Builder also support listeners registration in order to simulate setup/cleanup +lifecycle methods, common for test frameworks: + +```java +TestSupport.build(App.class) + .listen(new TestSupportBuilder.TestListener<>() { + public void setup(final DropwizardTestSupport support) throws Exception { + // do before test + } + ... + }) + .runCore(); +``` + +All listener methods are default so only required methods could be overridden. + +!!! warning + Builder could be used for support objects creation (`buildCore()`, `buildWeb()`) - + in this case listeners could not be used (only builder runner could properly process listeners). + +## Shortcuts + +For simple cases, there are many builder shortcuts in `TestSupport` class. + +Support object construction: + +```java +DropTestSupport support = TestSupport.webApp(App.class, + "path/to/config.yml", "prop: 1", "prop2: 2"); + +GuiceyTestSupport support = TestSupport.coreApp(App.class, + "path/to/config.yml", "prop: 1", "prop2: 2"); +``` + +Run: + +```java +RunResult result = TestSupport.runWebApp(App.class); + +RunResult result = TestSupport.runWebApp(App.class, + "path/to/config.yml", "prop: 1", "prop2: 2"); + +Object value = TestSupport.runWebApp(App.class, injector - > { + return injector.getInstance(Service.class).getSmth(); + }); + +Object value = TestSupport.runWebApp(App.class, + "path/to/config.yml", injector - > { + return injector.getInstance(Service.class).getSmth(); + }, "prop: 1", "prop2: 2"); + +// ... and same methods for "coreApp" +``` + +All these methods are builder shortcuts (suitable for simple cases). + +!!! tip + Context `DropwizardTestSupport` and `ClientSupport` objects could be statically referenced inside callback: + + ```java + TestSupport.runWebApp(App.class, injector - > { + DropwizardTestSupport support = TestSupport.getContext(); + ClientSupport client = TestSupport.getContextClient(); + return null; + }); + ``` + +## Asserting execution + +To assert configuration or any guicey bean it would be enough to use run without callback: + +```java +RunResult result = TestSupport.build(App.class).runCore(); + +// direct configuratio instance +Assertions.assertEquals(12, result.getConfiguration().getProp1()); +// any guice bean +Assertions.assertEquals(12, result.getBean(Configuration.class).getProp1()); +Assertions.assertNotNull(result.getEnvironment()); +Assertions.assertNotNull(result.getApplication()); +``` + +Web-related assertions could be done inside callback: + +```java +SomeRsponseObject res = TestSupport.build(App.class).runWeb(injecor -> { + ClientSupport client = TestSupport.getContextClient() + return client.get("some", SomeRsponseObject.class); + }); + +Assertions.assertEquals("something", res.getField1()) +``` + +Or multiple assertions could be done directly inside callback. + +## Managed lifecycle + +Core tests (without web part) simulate `Managed` objects lifecycle (because often core logic +rely on pre-initializations). + +If managed lifecycle is note required for test, it could be disabled with alternative run method: + +```java +TestSupport.build(App.class).runCoreWithoutManaged(..) +``` + +On raw test support object: `new GuiceyTestSupport().disableManagedLifecycle()` + +## Raw test support objects + +It may be required to use `DropwizardTestSupport` objects directly: for example, when before() and after() +calls must be performed in different methods (some test framework integration). + +Objects could be created with builder: + +```java +GuiceyTestSupport core = TestSupport.build(App.class) + .buildCore(); + +DropwizardTestSupport web = TestSupport.build(App.class) + .buildWeb(); +``` + +!!! note + `GuiceyTestSupport extends DropwizardTestSupport`, so in both cases + `DropwizardTestSupport` could be used as type. + +There are also shortcut methods: + +```java +DropwizardTestSupport supportCore = TestSupport.coreApp(App.class, + "path/to/config.yml", + "prop: 1", "prop2: 2"); + +DropwizardTestSupport supportWeb = TestSupport.webApp(App.class, + "path/to/config.yml", + "prop: 1", "prop2: 2"); +``` + +Support object usage: + +```java +support.before() + +// test + +support.after() +``` + +This is equivalent to: + +```java +TestSupport.run(support, injector -> { + // test +}); +``` + +Other helper methods for support object (executed while the support object is active): + +* `TestSupport.getInjector(support)` - obtain application injector +* `TestSupport.getBean(support, Key/Class)` - get guice bean +* `TestSupport.injectBeans(support, target)` - inject annotated object fields +* `TestSupport.webClient(support)` - construct `ClientSupport` object + +Support object provides references for dropwizard objects: + +```java +support.getEnvironment(); +support.getConfiguration(); +support.getApplication(); +``` + +Complete example using junit: + +```java +public class RawTest { + + static DropwizardTestSupport support; + + @Inject MyService service; + + @BeforeAll + public static void setup() { + support = TestSupport.coreApp(App.class); + // support = TestSupport.webApp(App.class); + // start app + support.before(); + } + + @BeforeEach + public void before() { + // inject services in test + TestSupport.injectBeans(support, this); + } + + @AfterAll + public static void cleanup() { + if (support != null) { + support.after(); + } + } + + @Test + public void test() { + Assertions.assertEquals("10", service.computeValue()); + } +} +``` + +!!! note + `support.before()` would automatically call `after()` in case of startup error diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/spies.md b/dropwizard-guicey/src/doc/docs/guide/test/general/spies.md new file mode 100644 index 000000000..62d19f759 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/spies.md @@ -0,0 +1,186 @@ +# Testing with spies + +[Mockito](https://site.mockito.org/) spies allows dynamic modification of real objects behavior +(configured same as [mocks](mocks.md), but, by default, all methods work as in raw bean). + +Guicey provides `SpiesHook` for overriding guice beans with mockito spies. + +!!! important + Spy creation requires real bean instance and so guicey use AOP to intercept real bean + access and redirecting all calls through a dynamically created (on first access) + spy object. This means that spies would only work with guice-managed beans. + + If you need to spy for a manual instance - use [partial mocks](mocks.md#partial-mocks) + +!!! warning + Spies will not work for HK2 beans + +Mockito documentation is written in the `Mockito` class [javadoc](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html). +Additional docs could be found in [mockito wiki](https://github.com/mockito/mockito/wiki/FAQ) +Also, see official [mockito refcard](https://dzone.com/refcardz/mockito) +and [baeldung guides](https://www.baeldung.com/mockito-series). + +## Setup + +Requires mockito dependency (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +Suppose we have a service: + +```java +public static class Service { + + public String get(int id) { + return "Hello " + id; + } +} +``` + +Spying it: + +```java +SpiesHook hook = new SpiesHook(); +// real spy could be created ONLY after injector startup +final SpyProxy proxy = hook.spy(Service.class); +// SpyProxy implements provider and so could be also used as: +// Provider provider = hook.spy(Service.class) + +TestSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + // get spy object for configuration + Service spy = proxy.getSpy(); + // IMPORTANT: spies configured in reverse order to avoid accidental method call + doReturn("bar1").when(spy).get(11); + + // real instance, injected everywhere in application (AOP proxy) + Service service = injector.getInstance(Service.class); + // stubbed result + Assertions.assertEquals("bar1", s1.get(11)); + // real method result (because argument is different) + Assertions.assertEquals("Hello 10", s1.get(10)); + }) +``` + +Here `doReturn` refer to `Mockito.doReturn` used with static import. + +!!! note + As real guice bean used under the hood, all AOP, applied to the original bean, will work. + +!!! tip + Calling guice proxy `injector.getInstance(Service.class).get(11)` and spy object + directly `spy.get(11)` is equivalent (because guice returns AOP proxy which redirects + call to the spy) + +See other examples in [mocks section](mocks.md#mocking-examples). + +## Asserting calls + +!!! tip + If you want to use spies to track bean access (verify arguments and response) then + try [trackers](tracks.md) which are better match for this case. + +As [mocks](mocks.md#asserting-calls), spies could be used to assert calls: + +```java +// method Service.get(11) called on mock just once +verify(spy, times(1)).get(11); +``` + +These assertions would fail if method was called more times or using different arguments. + +## Method result capture + +Verifying method return value with spies is a bit clumsy: + +```java +public static class ResultCaptor implements Answer { + private T result = null; + public T getResult() { + return result; + } + + @Override + public T answer(InvocationOnMock invocationOnMock) throws Throwable { + result = (T) invocationOnMock.callRealMethod(); + return result; + } +} + +ResultCaptor resultCaptor = new ResultCaptor<>(); +// capture actual argument value (just to show how to do it) +ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); +Mockito.doAnswer(resultCaptor).when(spy).get(argumentCaptor.capture()); + +// call method +Assertions.assertThat(spy.get(11)).isEqualTo("bar"); +// result captured +Assertions.assertThat(resultCaptor.getResult()).isEqualTo("bar"); +Assertions.assertThat(argumentCaptor.getValue()).isEqualTo(11); + +Mockito.verify(spy, Mockito.times(1)).get(11); +``` + +Why would you need that? It is often useful when verifying indirect bean call. +For example, if we have `SuperService` which internally calls `Service` and so +there is no other way to verify service call result correctness other than spying it (or use [tracker](tracks.md)). + +## Pre initialization + +As spy object creation is delayed until application startup, it is impossible to +configure spy before application startup (as with mocks). Usually it is not a problem, +if target bean is not called during startup. + +If you need to modify behavior of spy, used during application startup (e.g. by some `Managed`), +then there is a delayed initialization mechanism: + +```java +SpiesHook hook = new SpiesHook(); +// real spy could be created ONLY after injector startup +final SpyProxy proxy = hook.spy(Service.class) + .withInitializer(service -> doReturn("spied").when(service).get(11)); +... +``` + +Here, configuration from `withInitializer` block would be called just after +spy creation (on first access). + +And so any `Managed`, calling it during startup would use completely configured spy: + +```java +@Singleton +public static class Mng implements Managed { + @Inject + Service service; + + @Override + public void start() throws Exception { + // "spied" result + service1.get(11); + } +} +``` + +## Spies reset + +If you run multiple tests with the same application, then it makes sense to re-configure +spies for each test and so the previous spy state must be reset. + +Use `hook.resetSpies()` to reset all registered mocks + +## Accessing spy + +Spy instance (used to configure methods behavior) could be obtained: + +1. After application startup: + ```java + SpyProxy proxy = hook.spy(Service.class); + // after app startup + Service spy = proxy.getSpy(); + ``` +3. From hook: `Service spy = hook.getSpy(Service.class)` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/startup.md b/dropwizard-guicey/src/doc/docs/guide/test/general/startup.md new file mode 100644 index 000000000..18db30c9d --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/startup.md @@ -0,0 +1,34 @@ + +# Testing startup fails + +Command runner could also be used for application startup fail tests: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .run("server") +``` + +or with the shortcut: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .runApp() +``` + +!!! important + In case of application *successful* start, special check would immediately stop it + by throwing exception (resulting object would contain it), so such test would never freeze. + +!!! note "Why not run directly?" + You can run command directly: `new App().run("server")` + But, if application throws exception in *run* phase, `System.exit(1)` would be called: + + ```java + public abstract class Application { + ... + protected void onFatalError(Throwable t) { + System.exit(1); + } + } + ``` + Commands runner runs commands directly so exit would not be called. diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/stubs.md b/dropwizard-guicey/src/doc/docs/guide/test/general/stubs.md new file mode 100644 index 000000000..9ead0a797 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/stubs.md @@ -0,0 +1,122 @@ +# Testing with stubs + +Stubs are hand-made replacements of real application services ("manual" or "lazy" [mocks](mocks.md)). + +Guicey provides `StubsHook` for overriding guice beans with stub implementations. + +!!! warning + Stubs will not work for HK2 beans + +There are two main cases: + +1. Stub class extends existing service: `class ServiceStub extends Service` +2. Stub implements service interface: `class ServiceStub implements IService` + +Stubs replace real application services (using guice [overriding modules](../overview.md#guice-bindings-override)), +so stub would be injected in all services instead of the real service. + +For example, suppose we have a service: + +```java +public class Service { + public String foo() { + ... + } +} +``` + +where method foo implements some complex logic, not required in test. + +Writing stub: + +```java +public class ServiceStub extends Service { + @Override + public String foo() { + return "static value"; + } +} +``` + +Using stub in test: + +```java +StubsHook hook = new StubsHook(); +// register stub (stub instance managed with guice) +hook.stub(Service.class, ServiceStub.class); + +TestsSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + Service service = injector.getInstance(Service.class); + // service is a stub + Assertions.assertInstanceOf(ServiceStub.class, service); + Assertions.assertEquals("static value", service.foo()); + }); +``` + +!!! info + In many cases, mockito [mocks](mocks.md) and [spies](spies.md) could be more useful, + but stubs are simpler (easier to understand, especially comparing to spies). + +In the example above, stub instance is created by guice. +Stub could also be registered by instance: + +```java +hook.stub(Service.class, new ServiceStub()); +``` + +In this case, stub's `@Inject` fields would be processed (`requestInjection(stub)` would be called). + +!!! note + Guice AOP would apply only for stubs registered with class. So stub instance + could be used (instead of class) exactly to avoid additional AOP logic for service. + +## Stub lifecycle + +More complex stubs may contain a test-related internal state, which must be cleared between tests. + +In this case, stub could implement `StubLifecycle`: + +```java +public class ServiceStub extends Service implements StubLifecycle { + int calls; + + @Override + public void before() { + calls = 0; + } + + @Override + public void after() { + calls = 0; + } +} +``` + +(both methods optional) + +Then we can call these methods with hook: + +```java + +// call before() on all stubs +hook.before(); +// test staff here +// call after after test +hook.after(); +``` + +## Stub instance + +Stub instance could be obtained either from injector (using overridden service as a key): + +```java +ServiceStub stub = (ServiceStub) injector.getInstance(Service.class); +``` + +or directly from hook: + +```java +ServiceStub stub = hook.getStub(Service.class); +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/general/tracks.md b/dropwizard-guicey/src/doc/docs/guide/test/general/tracks.md new file mode 100644 index 000000000..34df14a5b --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/general/tracks.md @@ -0,0 +1,318 @@ +# Testing performance (bean tracking) + +Tracker records guice bean methods execution: + +1. Collect method call arguments and result for each call +2. Log slow methods execution +3. Collect metrics to show overall methods performance (stats) + +!!! warning + Trackers will not work for HK2 beans and for non guice-managed beans (bound by instance) + +!!! note + Initially, trackers were added as a simpler alternative for [mockito spy's + clumsy result capturing](spies.md#method-result-capture). But, eventually, it evolved into a simple performance tracking + tool + (very raw, of course, but in many cases it would be enough). + +## Setup + +Not strictly required, but trackers provide type-safe search api using mockito, and so +you'll need mockito dependency **only if** you wish to use this api (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +Suppose we have a service: + +```java +public static class Service { + + public String get(int id) { + return "Hello " + id; + } +} +``` + +And we want to very indirect service call (when service called by some other service): + +```java +TrackersHook hook = TrackersHook(); +final Tracker tracker = hook.track(Service.class) + .trace(true) + .add(); +TestSupport.build(DefaultTestApp .class) + .hooks(hook) + .runCore(injector ->{ + Service service = injector.getInstance(Service.class); + + // call service + Assertions.assertEquals("Hello 11",service.get(11)); + + MethodTrack track = tracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("get(11) = \"Hello 11\"")); + // object arguments + Assertions.assertArrayEquals(new Object[] {11},track.getRawArguments()); + // arguments in string form + Assertions.assertArrayEquals(new String[] {"11"},track.getArguments()); + // raw result + Assertions.assertEquals("1 call",track.getRawResult()); + // result in string form + Assertions.assertEquals("1 call",track.getResult()); + }); +``` + +In this example, trace was enabled (optional) and so each method call would be logged like this: + +``` +\\\---[Tracker] 0.41 ms <@1b0e9707> .get(11) = "Hello 11" +``` + +## Configuration + +Tracker registration call `final Tracker tracker = hook.track(Service.class)` +returns a configuration builder. Final registration appears only after `.add()` method call. + +| Option | Description | Default | +|---------------------------|-----------------------------------------------------------------------------------------------------------|-----------| +| trace | When enabled, all method calls are printed | false | +| slowMethods | Print warnings about methods executing longer than the specified threshold. Set to 0 to disable warnings. | 5 seconds | +| disableSlowMethodsLogging | Shortcut to disable tracking for slow methods (same as set 0). | | +| keepRawObjects | Keep method call arguments and result objects (potentially mutable) | true | +| maxStringLength | Max length for a `String` argument or result (cut long strings) | 30 | + +### Tracing + +Tracing might be useful to see each tracked method call in console with parameters and execution time: + +``` +\\\---[Tracker] 0.41 ms <@1b0e9707> .foo(1) = "1 call" +\\\---[Tracker] 0.02 ms <@1b0e9707> .foo(2) = "2 call" +\\\---[Tracker] 0.12 ms <@1b0e9707> .bar(1) = "1 bar" +``` + +It also prints service instance hash, to make obvious method calls on different instances. +Different instances could appear on prototype-scoped beans (default scope). + +Enabled with: + +```java +hook.track(Service .class) + .trace(true) +``` + +!!! note + Traces are logged with `System.out` to make sure messages are always visible in console. + +### Slow methods + +By default, tracker would log methods, executed longer than 5 seconds: + +``` +WARN [2025-05-09 08:30:38,458] ru.vyarus.dropwizard.guice.test.track.Tracker: +\\\---[Tracker] 7.07 ms <@7634f2b> .foo() = "foo" +``` + +!!! note + Slow methods are logged with **logger**, and not `System.out` as traces. + +For example, to set slow method for 1 minute: + +```java +hook.track(Service .class) + .slowMethods(1, ChronoUnit.MINUTES) +``` + +To avoid logging slow methods (shortcut for setting 0 value): + +```java +hook.track(Service .class) + .disableSlowMethodsLogging() +``` + +### Keeping raw objects + +By default, tracker stores all arguments and returned result objects. + +Raw arguments could be used to examine complex objects just after the method call. +But, in case of multiple method calls, raw objects might not be actual. For example: + +```java +public Service { + public void foo(List list) { + list.add("foo" + list.size()); + } +} +``` + +Here method changes argument state and so, if we call method multiple times, stored arguments +would be useless (as all calls would reference the same list instance): + +```java +List test = new ArrayList<>(); +service.foo(test); +service.foo(test); + +// stored list useless as object was changed after the initial call +List firstCallArg = tracker.getLastTracks(2).get(0).getRawArguments().get(0); +Assertions.assertEquals(2, firstCallArg.size()); + +// but string representation would still be useful: +String firstCallArgString = tracker.getLastTracks(2).get(0).getArguments().get(0); +Assertions.assertEquals("0[]", firstCallArg.size()); + +// second call argument string +String firstCallArgString = tracker.getLastTracks(2).get(1).getArguments().get(0); +Assertions.assertEquals("1['foo1']", firstCallArg.size()); +``` + +In case of complex objects (pojo, for example), string representation would only contain +the type and instance hash: `Type@hash` (which is not informative, but the only universal short +way to describe object). + +If tracker used only for performance testing (to accumulate execution time from many runs), +it might make sense to avoid holding raw arguments: + +```java +hook.track(Service .class) + .keepRawObjects(false) +``` + +### Max length + +Methods could consume or return large string, but using large stings for console +output is not desired. All strings larger then configured size would be cut with "..." suffix: + +``` +\\\---[Tracker] 0.08 ms <@66fb45e5> .baz("largelargelargelargelargelarge...") +``` + +Changing default: + +```java +hook.track(Service .class) + .maxStringLength(10) +``` + +## Tracked data + +Each call stored as `MethodTrack` and contains raw arguments `getRawArguments()` (which might change over time +if mutable objects used) and string version `getArguments()` (can't change) and same for the result object. +Raw objects are mostly useful in case of immediate check after the method call. + +Same for result: `getRawResult()` for raw object and `getResult()` for string version. + +Also, there are quoted string versions: `getQuatedResult()` and `getQuatedArguments()`. +These methods are the same as string methods, but all strings are in quotes to clearly see +string bounds (quoted versions useful for console reporting) + +Obtaining tracked data: + +```java +// all recordings +List tracks = tracker.getTracks(); +// last 2 calls (in execution order) +List tracks = tracker.getLastTracks(2); +// last call +MethodTrack track = tracker.getLastTrack(); +``` + +### Searching + +In the case of many recorded executions (for multiple methods), search could be used: + +```java +// search by method (any argument value) +tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.anyInt())) + ); + +// search methods with argument condition ( > 1) +tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument > 1))) + ); + +// search for methods with exact argument value +tracks = tracker.findTracks(mock -> when( + mock.foo(11)) + ); +``` + +This method uses Mockito stubbing abilities for search criteria declaration: +easy to use and type-safe search. + +### Reset data + +Tracked data could be cleared at any time either on tracker: `tracker.clear()` +or using hook (for all trackers): `hook.resetTrackers()` + +## Stats + +Tracker could aggregate all executions of the same method: + +```java +TrackerStats stats = tracker.getStats(); +Assertions.assertEquals(1, stats.getMethods().size()); + +MethodSummary summary = stats.getMethods().get(0); +Assertions.assertEquals("foo", summary.getMethod().getName()); +Assertions.assertEquals(Service.class, summary.getService()); +Assertions.assertEquals(1, summary.getTracks()); +Assertions.assertEquals(0, summary.getErrors()); +Assertions.assertEquals(1, summary.getMetrics().getValues().length); +Assertions.assertTrue(summary.getMetrics().getMin() < 1000); +``` + +Tracker use dropwizard metrics, so stats provide common values like mean time, median time, 95 percentile, etc. + +There is a default statistics report implementation, which might be used for console reporting: + +```java +System.out.println(tracker.getStats().render()); +``` + +```java + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + Service foo(int) 2 (2) 0 0.009 ms 0.352 ms 0.352 ms 0.352 ms 0.352 ms +``` + +Here you can see that 2 instances were used for 2 success calls. Of course max time +would be too large (cold jvm), but with min value you can see more realistic time. +With a high number of executions percentile and mean values would become more realistic. + +Here is an example of tracking `GuiceyConfigurationInfo` with activated `.printAllGuiceBindings()` report: + +``` + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + GuiceyConfigurationInfo getNormalModuleIds() 1 0 1.076 ms 1.076 ms 1.076 ms 1.076 ms 1.076 ms + GuiceyConfigurationInfo getModulesDisabled() 1 0 0.038 ms 0.038 ms 0.038 ms 0.038 ms 0.038 ms + GuiceyConfigurationInfo getOverridingModuleIds() 1 0 0.034 ms 0.034 ms 0.034 ms 0.034 ms 0.034 ms + GuiceyConfigurationInfo getExtensionsDisabled() 1 0 0.020 ms 0.020 ms 0.020 ms 0.020 ms 0.020 ms + GuiceyConfigurationInfo getOptions() 1 0 0.005 ms 0.005 ms 0.005 ms 0.005 ms 0.005 ms + GuiceyConfigurationInfo getData() 3 0 0.003 ms 0.006 ms 0.004 ms 0.006 ms 0.006 ms + +``` + +!!! note + Methods sorted by slowness + +You can also collect stats for multiple trackers: + +```java +TrackerStats overall = new TrackerStats(tracker1, tracker2); +System.out.println(overall.render()); +``` + +## Tracker object access + +If required, existing tracker object could be obtained directly from hook: + +```java +Tracker tracker = hook.getTracker(Service.class); +``` + +This might be useful, for example, to obtain multiple trackers and print overall stats. +But, in the majority of cases, tracker instance, created on registration would be enough. \ No newline at end of file diff --git a/src/doc/docs/guide/test/junit4.md b/dropwizard-guicey/src/doc/docs/guide/test/junit4.md similarity index 90% rename from src/doc/docs/guide/test/junit4.md rename to dropwizard-guicey/src/doc/docs/guide/test/junit4.md index 4c0417a72..62dea56ed 100644 --- a/src/doc/docs/guide/test/junit4.md +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit4.md @@ -1,7 +1,7 @@ # Junit 4 !!! warning - Since guicey 5.5 junit 4 support was extracted from guicey to [external module](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-test-junit4): + Since guicey 5.5 junit 4 support was extracted from guicey to [external module](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-test-junit4): * Package remains the same to simplify migration (only additional dependency would be required) * Deprecation marks removed from rules to reduce warnings. @@ -40,7 +40,7 @@ Provided rules: ## Testing core logic For integration testing of guice specific logic you can use `GuiceyAppRule`. It works almost like -[DropwizardAppRule](https://www.dropwizard.io/en/release-2.0.x/manual/testing.html#id2), +[DropwizardAppRule](https://github.com/dropwizard/dropwizard-testing-junit4), but *doesn't start jetty* (and so jersey and guice web modules will not be initialized). Managed and lifecycle objects supported. @@ -66,7 +66,7 @@ new GuiceyAppRule<>(MyApplication.class, null) ## Testing web logic For web component tests (servlets, filters, resources) use -[DropwizardAppRule](https://www.dropwizard.io/en/release-2.0.x/manual/testing.html#id2). +[DropwizardAppRule](https://github.com/dropwizard/dropwizard-testing-junit4). To access guice beans use injector lookup: @@ -216,15 +216,15 @@ create call). ## Migrating to JUnit 5 -* Instead of `GuiceyAppRule` use [@TestGuiceyApp](junit5.md#testguiceyapp) extension. -* Instead of `DropwizardAppRule` use [@TestDropwizardApp](junit5.md#testdropwizardapp) extension. -* `GuiceyHooksRule` can be substituted with hooks declaration [in extensions](junit5.md#application-test-modification) or as [test fields](junit5.md#hook-fields) +* Instead of `GuiceyAppRule` use [@TestGuiceyApp](junit5/run.md#testing-core-logic) extension. +* Instead of `DropwizardAppRule` use [@TestDropwizardApp](junit5/run.md#testing-web-logic) extension. +* `GuiceyHooksRule` can be substituted with hooks declaration [in extensions](junit5/hooks.md) or as [test fields](junit5/hooks.md#hook-fields) * Instead of `StartupErrorRule` use [system-stubs](https://github.com/webcompere/system-stubs) - the successor of system rules In essence: * Use annotations instead of rules (and forget about RuleChain difficulties) * Test fields injection will work out of the box, so no need for additional hacks -* JUnit 5 propose [parameter injection](junit5.md#parameter-injection), which may be not common at first, but its actually very handy +* JUnit 5 propose [parameter injection](junit5/inject.md#parameter-injection), which may be not common at first, but it's actually very handy -Also, there is a pre-configured [http client](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#client) suitable for calling test application urls (or any other general url). \ No newline at end of file +Also, there is a pre-configured [http client](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#client) suitable for calling test application urls (or any other general url). diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/client.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/client.md new file mode 100644 index 000000000..05dbb271f --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/client.md @@ -0,0 +1,848 @@ + +# Testing web (HTTP client) + +Both extensions prepare special jersey client instance which could be used for web calls. +It is mostly useful for integration tests to call rest services and servlets. + +`ClientSupport` could only be injected as test/setup method parameter: + +```java +public void setup(ClientSupport client) { } + +@Test +void test(ClientSupport client) { } +``` + +or in field: + +```java +@WebClient +ClientSupport client; +``` + +Object is a wrapper above [JerseyClient](https://eclipse-ee4j.github.io/jersey.github.io/documentation/2.29.1/client.html) to automate base url resolution from current configuration. + +!!! note + By default, app context is on port 8080 and admin context is on port 8081. + For simple server both admin and app contexts located on the same port (8080). + + There are 3 configurations, related to context paths: + + * `server.applicationContextPath` - application context + * `server.rootPath` - rest context (relative to app context) + * `server.adminContextPath` - admin context + + By default, all contexts are "/". + + ClientSupport povides access to resolved configuration with: + + ```java + client.getPort() // app port (8080) + client.getAdminPort() // app admin port (8081) + client.basePathRoot() // root server path (http://localhost:8080/) + client.basePathApp() // app context path (http://localhost:8080/) + client.basePathAdmin() // admin context path (http://localhost:8081/) + client.basePathRest() // rest context path (http://localhost:8080/) + ``` + +`ClientSupport` also provides 3 sub clients: + +```java +// http://localhost:{port}/{appContext}/ +TestClient app = client.appClient(); +// http://localhost:{port}/{adminContext}/ +TestClient admin = client.adminClient(); +// http://localhost:{port}/{appContext}/{restContext}/ +TestClient rest = client.restClient(); +``` + +By using these clients, you could use context-related urls, not worrying about potential configuration changes: + +```java +// GET {rest path}/some +Some res = client.restClient().get("some", Some.class); + +// GET {main context path}/servlet +String res = client.appClient().get("servlet", String.class); + +// GET {admin context path}/adminServlet +String res = client.adminClient().get("adminServlet", String.class); +``` + +An additional client could be created for remote api (or resource) calling: + +```java +// General external url call +String res = client.externalClient("https://google.com").get("/", String.class); +``` + +Specific clients could also be directly injected as fields: + +```java +// same as client.appClient() +@WebClient(WebClientType.App) +TestClient app; + +// same as client.adminClient() +@WebClient(WebClientType.Admin) +TestClient admin; + +// same as client.restClient() +@WebClient(WebClientType.Rest) +TestClient rest; +``` + +All these clients (including `ClientSupport` itself) use the same `TestClient` class, providing +all required methods to call web resources. + +!!! tip + [Lightweight rest](stubs.md) client (RestClient) is also based on `TestClient` so clients for integration and lightweight tests + are completely the same. + +## Shortcuts + +`TestClient` contains simplified GET/POST/PUT/PATCH/DELETE shortcut methods: + +!!! tip + For rest testing prefer [lightweight rest](rest.md) - these tests are faster because the real web server is + not started (no rela web calls - they are simulated). This is an official jersey testing api. + + +```java +@Test +public void testWeb(ClientSupport client) { + TestClient rest = client.restClient(); + + // get with simple result + Result res = client.get("/sample", Result.class); + // get with simple result list + List res = client.get("/list", new GenericType<>() {}); + + // post without result (void) + client.post("/post", new PostObject()); + + // post with result + Result res = client.post("rest/action", new PostObject(), Result.class); +} +``` + +POST/PUT/PATCH could accept raw entities (converted to json) or custom `Enitity` objects: + +```java +client.post("rest/action", Entity.text("text"), Result.class); +``` + +There is also a void variation for these methods (when response is not important): + +```java +client.post("rest/action", Entity.text("text")); +``` + +Such methods only verify that the response was successful. + +!!! tip + All client methods support `String.format` for path variables processing: + + ```java + client.get("/some/%s", User.class, 12) + ``` + +## Defaults + +Each `TestClient` provide "default*" methods to set request defaults: + +* `defaultHeader("Name", "value")` +* `defaultQueryParam("Name", "value")` +* `defaultCookie("Name", "value")` +* `defaultAccept("application/json")` +* etc. + +The most obvious use case is authorization: + +```java +@WebClient(WebClientType.Rest) +TestClient rest; + +@BeforeTest +public void setup() { + rest.defaultHeader("Authorization", "Bearer 123"); +} + +@Test +public void testSomething() { + User user = rest.get("/users/123", User.class); +} +``` + +Defaults could be cleared at any time with `client.reset()`. + +!!! note + When using filed injection for client, defaults would be cleared after each test method. + This could be disabled with `@WebClient(autoRest=false)`. + When the client is injected as method parameter (`public void test(ClientSupport client)`) + reset is not called automatically. + +## Sub clients + +There is a concept of sub clients. It is used to create a client for a specific sub-url. +For example, suppose all called methods in test have some base path: `/{somehting}/path/to/resource`. +Instead of putting it into each request: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient(); + + rest.get("/%s/path/to/resource/%s", User.class, "path", 12); + rest.post("/%s/path/to/resource/%s", new User(...), "path", 12); +} +``` + +A sub client could be created: + +```java +public void testSomething(ClientSupport client) { + TestClient rest = client.restClient().subClient("/{something}/path/to/resource") + .defaultPathParam("something", "path"); + + rest.get("/%s", User.class, "path", 12); + rest.post("/%s", new User(...), "path", 12); +} +``` + +!!! note + Sub clients inherit defaults of parent client. + + ```java + client.defaultQueryParam("q", "v"); + TestClient rest = client.subClient("/path/to/resource"); + + // inherited query parameter q=v will be applied to all requests + rest.get("/%s", User.class, 12); + ``` + +There is a special sub client creation method using jersey `UriBuilder`, required +to properly support matrix parameters in the middle of the path: + +```java +TestClient sub = client.subClient(builder -> builder.path("/some/path").matrixParam("p", 1)); + +// /some/path;p=1/users/12 +sub.get("/users/%s", User.class, 12); +``` + +## Builder API + +Request builder API covers all possible configurations +for jersey `WebTarget` and `Invocation.Builder`. The main idea was to simplify +request configuration: to provide all possible methods in one place. + +For example: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .as(User.class) +``` + +Request specific extensions and properties are also supported: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .register(VoidBodyReader.class) + .property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE) + .asVoid(); +``` + +All builder methods start with a "build" prefix (`buildGet()`, `buildPost()` or generic `build()`). + +Builder provides direct value mappings: + +* `.as(Class)` +* `.as(GenericType)` +* `.asVoid()` +* `.asString()` + +And methods, returning raw (wrapped) response: + +* `.invoke()` - response without status checks +* `.expectSuccess()` - fail if not success +* `.expectSuccess(201, 204)` - fail if not success or not expected status +* `.expectRedirect()` - fail if not redirect (method also disabled redirects following) +* `.expectRedirect(301)` - fail if not redirect or not expected status +* `.expectFailure()` - fail if success +* `.expectFailure(400)` - fail success or not expected status + +## Debug + +Considering the client defaults inheritance (potential decentralized request configuration), +it might be unobvious what was applied to the request. + +Request builder provides a `debug()` option, which will print all applied defaults +and direct builder configurations to the console: + +```java +client.buildGet("/path") + .queryParam("q", "v") + .debug() + .as(User.class) +``` + +``` +Request configuration: + + Path params: + p1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:61) + p2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + p3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62) + + Query params: + q1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:57) + q2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + q3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58) + + Accept: + application/json at r.v.d.g.t.c.builder.(RequestBuilderTest.java:54) + +Jersey request configuration: + + Resolve template at r.v.d.g.t.c.builder.(TestRequestConfig.java:869) + (encodeSlashInPath=false encoded=true) + p1=1 + p2=2 + p3=3 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q1=1 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q2=2 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q3=3 + + Accept at r.v.d.g.t.c.builder.(TestRequestConfig.java:899) + [application/json] +``` + +It shows two blocks: + +* How request builder was configured (including defaults source) +* How jersey request was configured + +The latter is obtained by wrapping jersey `WebTarget` and `Invocation.Builder` +objects to intercept all calls. + +Debug could be enabled for all requests: `client.defaultDebug(true)`. + +## Request assertions + +It would not be very useful for the majority of cases, but as debug api could +aggregate all request configuration data, it is possible to assert on it: + +```java +client.buildGet("/some/path") + .matrixParam("p1", "1") + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("/some/path;p1=1")) + .as(SomeEntity.class); +``` + +or + +```java +.assertRequest(tracker -> assertThat(tracker.getQueryParams().get("q")).isEqualTo("1")) +``` + +## Response assertions + +Request builder methods like `.invoke()` or `.expectSuccess()` returns +a special response wrapper object. It provides a lot of useful assertions to simplify +response data testing (avoid boilerplate code). + +For example, check a response header, cookie and obtain value + +```java +User user = rest.buildGet("/users/123") + .expectSuccess() + .assertHeader("Token" , s -> s.startsWith("My-Header;")) + .assertCookie("MyCookie", "12") + .as(User.class); +``` + +Here assertion error will be thrown if header or cookie was not provided or condition does not match. + +Even if you need to obtain a header or cookie value from response, you can use assetions to verify +header/cookie presence: + +```java +Response response = rest.buildGet("/users/123") + .expectSuccess() + .assertHeader("Token" , s -> s.startsWith("My-Header;")) + .asResponse(); + +// here you could be sure the header exists +String token = response.getHeaderString("Token"); +``` + +Redirection correctness could be checked as: + +```java +@Path("/resources") +public class Resource { + + @Inject + AppUrlBuilder urlBuilder; + + @Path("/list") + @GET + public Response get() { + ... + } + + @Path("/redirect") + @GET + public Response redirect() { + return Response.seeOther( + urlBuilder.rest(SuccFailRedirectResource.class).method(Resource::get).buildUri() + ).build(); + } +} +``` + +```java +rest.method(Resource::redirect) + // throw error if not 3xx; also, this disables redirects following + .expectRedirect() + .assertHeader("Location", s -> s.endsWith("/resources/list")); +``` + +Also, "with*" methods could be used for completely manual assertions: + +```java +rest.method(Resource::redirect) + .expectSuccess(201) + .withHeader("MyHeader", s -> + assertThat(s).startsWith("My-Header;")); +``` + +Response object could be converted without additional variables: + +```java +String value = rest.method(Resource::redirect) + .expectSuccess() + .as(res -> res.readEntity(SomeClass.class).getProperty()); +``` + +## Form builder + +There is a special builder helping build urlencoded and multipart requests (forms): + +```java +// urlencoded +client.buildForm("/some/path") + .param("name", 1) + .param("date", 2) + .buildPost() + .as(String.class); + +// multipart +client.buildForm("/some/path") + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildPost() + .asVoid(); +``` + +!!! tip + Compare with raw jersey api usage: + + ```java + FormDataMultiPart multiPart = new FormDataMultiPart(); + multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + + FileDataBodyPart fileDataBodyPart = new FileDataBodyPart("file", + file.toFile(), + MediaType.APPLICATION_OCTET_STREAM_TYPE); + multiPart.bodyPart(fileDataBodyPart); + + rest.post(path, Entity.entity(multiPart, multiPart.getMediaType()), Something.class); + ``` + +Also, it could be used to simply create a request entity and use it directly: + +```java +Entity entity = client.buildForm(null) + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildEntity() + +client.post("/some/path", entity); +``` + +Builder will serialize all provided (non-multipart) parameters to string. +For dates, it is possible to specify a custom date format: + +```java +client.buildForm("/some/path") + .dateFormat("dd/MM/yyyy") + .param("date", new Date()) + .param("date2", LocalDate.now()) + .buildPost() + .asVoid(); +``` + +(java.util and java.time date formatters could be set separately with `dateFormatter()` or `dateTimeFormatter()` methods) + +The default format could be changed globally: `client.defaultFormDateFormat("dd/MM/yyyy")` +(or `defaultFormDateFormatter()` with `defaultFormDateTimeFormatter()`). + +## Jersey API + +It is possible to use `client.target("/path")` to build raw jersey target +(with the correct base path). But without applied defaults. + +`ClientSupport` also provide shortcuts for context-specific targets: + +```java +// GET {rest path}/some +client.targetRest("some").request().buildGet().invoke() + +// GET {main context path}/servlet +client.targetApp("servlet").request().buildGet().invoke() + +// GET {admin context path}/adminServlet +client.targetAdmin("adminServlet").request().buildGet().invoke() + +// General external url call +client.target("https://google.com").request().buildGet().invoke() +``` + +Direct `Invocation.Builder` (with applied defaults) could be built with: + +```java +// base url would be a current client's url +client.request("/path").buildGet().invoke(); +``` + +Builder API does not hide native jersey API: + +* `WebTarget` - could be modified directly with `request.configurePath(target -> target.path("foo"))` +* `Invocation.Builder` - with `request.configureRequest(req -> req.header("foo", "bar"))` + +Such modifiers could be applied as client defaults: + +* `client.defaultPathConfiguration(...)` +* `client.defaultRequestConfiguration(...)` + +Response wrapper also provides direct access to jersey `Response` object: +`response.asResponse()`. + +## Resource clients + +There is a special type of type-safe clients based on the simple idea: +resource class declaration already provides all required metadata to configure a test request: + +```java +@Path("/users") +public class UserResource { + + @Path("/{id}") + @GET + public User get(@NotNull @PathParam("id") Integer id) {} +} +``` + +Resource declares its path in the root `@Path` annotation and method annotations +tell that it's a GET request on path `/users/{id}` with required path parameter. + +```java +// essentially, it's a sub client build with the resource path (from @Path annotation) +ResourceClient rest = client.restClient(UserResource.class); + +User user = rest.method(r -> r.get(123)).as(User.class); +``` + +By using a mock object call (`r -> r.get(123)`) we specify a source of metadata and the required values +for request. Using it, a request builder is configured automatically. + +It is not required to use all parameters (reverse mapping is not always possible): +use null for not important arguments. All additional configurations could be done manually: + +```java +ResourceClient rest = client.restClient(UserResource.class); + +User user = rest.method(r -> r.get(null)) + .pathParam("id", 123) + .as(User.class); +``` + +Almost everything could be recognized: + +* All parameter annotations like `@QueryParam`, `@PathParam`, `@HeaderParam`, `@MatrixParam`, `@FormParam`, etc. +* All request methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. +* Request body mapping: `void post(MyEntity entity)` +* And even multipart forms + +Not related arguments should be simply ignored: + +```java +public void get(@PathParam("id") Integer id, @Context HttpServletRequest request) {} + +rest.method(r -> r.get(123, null)); +``` + +!!! note + `ResourceClient` extends `TestClient`, so all usual method shortcuts are also available for resource client + (real method calls usage is not mandatory). + +Resource client could be directly injected as a test field +(instead of calling `client.resourceClient(MyResource.class)`: + +```java +@WebResourceClient +ResourceClient rest; +``` + +### Multipart forms + +Multipart resource methods often use special multipart-related entities, like: + +```java + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) +``` + +Which is not handy to create manually. To address this, `ResourceClient` provides a +special helper object to build multipart-related values: + +```java +rest.multipartMethod((r, multipart) -> + r.multipart(multipart.fromClasspath("/sample.txt"), + multipart.disposition("file", "sample.txt")) + .asVoid()); +``` + +Here file stream passed as a first parameter and filename with the second one. + +Or + +```java + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2( + @NotNull @FormDataParam("file") FormDataBodyPart file) +``` + +```java + rest.multipartMethod((r, multipart) -> + r.multipart2(multipart.streamPart("file", "/sample.txt"))) + .asVoid(); +``` + +In case of generic multipart object argument: + +```java + @Path("/multipartGeneric") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartGeneric(@NotNull FormDataMultiPart multiPart) +``` + +there is a special builder: + +```java +rest.multipartMethod((r, multipart) -> + r.multipartGeneric(multipart.multipart() + .field("foo", "bar") + .stream("file", "/sample.txt") + .build())) + .as(String.class); +``` + +!!! note + Multipart methods require the urlencoded client (default) and, most likely, + will fail with the apache client. + +### Sub resources + +When a sub resource is declared with an instance: + +```java +public class Resource { + @Path("/sub") + public SubResource sub() { + return new SubResource(); + } +} +``` + +it could be easily called directly: + +```java +User user = rest.method(r -> r.sub().get(123)).as(User.class); +``` + +When a sub resource method is using class: + +```java +public class Resource { + @Path("/sub") + public Class sub() { + return SubResource.class; + } +} +``` + +you'll have to build a sub-client first: + +```java +ResourceClient subRest = rest.subResource(Resource::sub, SubResource.class); +``` + +!!! important + Jersey ignores sub-resource `@Path` annotation, so special method for sub resource clients is required. + +### Resource typification + +It is not always possible to use resource class to buld a sub client +(with `.restClient(Resource.class)`). + +In such cases you can build a resource path manually and then "cast" client to the resource type: + +```java +ResourceClient rest = client.subClient("/resource/path") + .asResourceClient(MyResource.class); +``` + +or just build path manually: + +```java +ResourceClient rest = client.subClient( + builder -> builder.path("/resource").matrixParam("p", 123), + MyResource.class); +``` + +## Apache client + +By default, the client is based on "url connector", which has a limitation for PATCH +requests: on java > 16 PATCH requests will not work without additional `--add-opens`. +For such requests it is easier to use an apache connector. + +It is not possible to use apache connector by default because it +[has problems](https://github.com/eclipse-ee4j/jersey/issues/5528#issuecomment-1934766714) +with multipart requests). + +You can switch connector type either by providing different `TestClientFactory` +or by calling `ClientSupport` shortcuts: + +* `client.apacheClient()` - `ClientSupport` with apache connector +* `client.urlconnectorClient()` - `ClientSupport` with url connector + +With these shortcuts you can use both connectors in the same test. + +## Client factory + +`JerseyClient` used in `ClientSupport` could be customized using `TestClientFactory` implementation. + + +Simple factory example: + +```java +public class SimpleTestClientFactory implements TestClientFactory { + + @Override + public JerseyClient create(final DropwizardTestSupport support) { + return new JerseyClientBuilder() + .register(new JacksonFeature(support.getEnvironment().getObjectMapper())) + .property(ClientProperties.CONNECT_TIMEOUT, 1000) + .property(ClientProperties.READ_TIMEOUT, 5000) + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) + .build(); + } +} +``` + +or using the default implementation as base: + +```java +public class SimpleTestClientFactory extends DefaultTestClientFactory { + + @Override + protected void configure(final JerseyClientBuilder builder, final DropwizardTestSupport support) { + builder.getConfiguration().connectorProvider(new Apache5ConnectorProvider()); + } +} +``` + +Default implementation (`DefaultTestClientFactory`) applies timeouts and auto-registers multipart support if `dropwizard-forms` module +if available in classpath. + +Custom implementation could be specified directly in the test annotation: + +```java +@TestDropwizardApp(value = MyApp.class, clientFactory = CustomTestClientFactory.class) +``` + +(or `.clientFactory()` method in builder) + +### Default client + +`JerseyClient` used inside `ClientSupport` is created by `DefaultTestClientFactory`. + +Default implementation: + +1. Enables multipart feature if `dropwizard-forms` is in classpath (so the client could be used + for sending multipart data). +2. Enables request and response logging to simplify writing (and debugging) tests. + +By default, all request and response messages are written directly into console to guarantee client +actions visibility (logging might not be configured in tests). + +Example output: + +``` + +[Client action]---------------------------------------------{ +1 * Sending client request on thread main +1 > GET http://localhost:8080/sample/get + +}---------------------------------------------------------- + + +[Client action]---------------------------------------------{ +1 * Client response received on thread main +1 < 200 +1 < Content-Length: 13 +1 < Content-Type: application/json +1 < Date: Mon, 27 Nov 2023 10:00:40 GMT +1 < Vary: Accept-Encoding +{"foo":"get"} + +}---------------------------------------------------------- +``` + +Console output might be disabled with a system proprty: + +```java +// shortcut sets DefaultTestClientFactory.USE_LOGGER property +DefaultTestClientFactory.disableConsoleLog() +``` + +With it, everything would be logged into `ClientSupport` logger (java.util) under INFO +(most likely, would be invisible in the most logger configurations, but could be enabled). + + +To reset property (and get logs back into console) use: + +```java +DefaultTestClientFactory.enableConsoleLog() +``` + +!!! note + Static methods added not directly into `ClientSupport` because this is + the default client factory feature. You might use a completely different factory. diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/command.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/command.md new file mode 100644 index 000000000..5bc311fb1 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/command.md @@ -0,0 +1,132 @@ +# Testing commands + +!!! warning + Commands execution overrides System IO and so can't run in parallel with other tests! + + Use [`@Isolated`](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization) + on such tests to prevent parallel execution with other tests + +Command execution is usually a short-lived action, so it is not possible to +write an extension for it. Command could be tested only with generic utility: + +```java +@Test +public class testCommand() { + CommandResult result = TestSupport.buildCommandRunner(App.class) + .run("cmd", "-p", "param"); + + Assertions.assertTrue(result.isSuccessful()); +} +``` + +This runner could be used to run *any* command type (simple, configured, environment). +The type of command would define what objects would be present ofter the command execution +(for example, `Injector` would be available only for `EnvironmentCommand`). + +Run command arguments are the same as real command arguments (the same `Cli` used for commands parsing). +You can only omit configuration path and use builder instead: + +```java + CommandResult result = TestSupport.buildCommandRunner(App.class) + .config("path/to/config.yml") + .configOverride("prop: 1") + .run("cmd", "-p", "param"); +``` + +!!! important + Command execution never throws an exception - any appeared exception would be + inside resulted object: + + ```java + Assertions.assertFalse(result.isSuccessful()); + Assertions.assertEquals("Error message", result.getException().getMessage()); + ``` + + Output is intercepted and could be used for assertions: + ```java + Assertions.assertTrue(result.getOutput().contains("some text")) + ``` + + All special objects (like configuration, environment etc), created during command execution + are all stored inside the result object (this is the only way to access them). + +### IO + +Runner use System.in/err/out replacement. All output is intercepted and could be +asserted: + +```java +Assertions.assertTrue(result.getOutput().contains("some text")) +``` + +`result.getOutput()` contains both `out` and `err` streams together +(the same way as user would see it in console). Error output is also available +separately with `result.getErrorOutput()`. + +!!! note + All output is always printed to console, so you could always see it after test execution + (without additional actions) + +Commands requiring user input could also be tested (with mocked input): + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .consoleInputs("1", "two", "something else") + .run("quiz") +``` + +At least, the required number of answers must be provided (otherwise error would be thrown, +indicating not enough inputs) + +!!! warning + Due to IO overrides, command tests could not run in parallel. + For junit 5, such tests could be annotated with [`@Isolated`](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization) + (to prevent execution in parallel with other tests) + +### Configuration + +Configuration options are the same as in run builder. For example: + +```java +// override only +TestSupport.buildCommandRunner(App.class) + .configOverride("foo: 12") + .run("cfg"); + +// file with overrides +TestSupport.buildCommandRunner(App.class) + .config("src/test/resources/path/to/config.yml") + .configOverride("foo: 12") + .run("cfg"); + +// direct config object +MyConfig config = new MyConfig(); +TestSupport.buildCommandRunner(App.class) + .config(config) + .run("cfg"); +``` + +!!! note + Config file should not be specified in command itself - builder would add it, if required. + But still, it would not be a mistake to use config file directly in command: + + ```java + TestSupport.buildCommandRunner(App.class) + // note .config("...") was not used (otherwise two files would appear)! + .run("cfg", "path/to/config.yml"); + ``` + + Using builder for config file configuration assumed to be a preferred way. + +### Listener + +There is a simple listener support (like in application run builder) for setup-cleanup actions: + +```java +TestSupport.buildCommandRunner(App.class) + .listen(new CommandRunBuilder.CommandListener<>() { + public void setup(String[] args) { ... } + public void cleanup(CommandResult result) { ... } + }) + .run("cmd") +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/config.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/config.md new file mode 100644 index 000000000..49f29456b --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/config.md @@ -0,0 +1,194 @@ +# Application configuration + +!!! note + In terms of configuration, both extensions (`@TestGuiceyApp` and `@TestDropwizardApp`) + are equal, so all examples would show just one of them. + + Also, annotation provides the same options as field-based extension declaration, + so if something is shown for annotation - the same could be done with builder. + +Application could be started with an external configuration file: + +```java +@TestGuiceyApp(value = MyApplication.class, + config = "path/to/my/test-config.yml" +public class MyTest { +``` + +Or just declare required values: + +```java +@TestGuiceyApp(value = MyApplication.class, + configOverride = { + "foo: 2", + "bar: 12" + }) +public class ConfigOverrideTest { +``` + +(note that overriding declaration follows yaml format "key: value") + +Or use both at once (here overrides will override file values): + +```java +@TestGuiceyApp(value = MyApplication.class, + config = 'path/to/my/config.yml', + configOverride = { + "foo: 2", + "bar: 12" + }) +class ConfigOverrideTest { +``` + +## Manual configuration object + +Normally, either empty configuration object created (if a configuration file not provided) +or it created from a specified file. + +It is also possible to manually construct configuration object instance in +junit5 extension (for both lightweight and full app tests): + +```java +@RegisterExtension +static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..) + .config(() -> new MyConfig()) + ... +``` + +Or in setup object: + +```java +@EnableSetup +static TestEnvironmentSetup setup = ext -> ext.config(() -> new MyConfig()) +``` + +!!! tip + Pay attention to how setup objects could be used for configuration modification: + it is often easier to declare test extension in base class and use setup objects + for test-specific modifications. + +!!! important + Configuration overrides **would not work** with manually created configuration objects. + Use configuration modifiers with manual configs. + + +## Configuration modifiers + +Dropwizard configuration overrides mechanism is limited (for example, it would not work for a collection property). + +Configuration modifier is an alternative mechanism when all changes are performed on +configuration instance. + +Modifier could be used as lambda: + +```java +@RegisterExtension +static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..) + .configModifiers(config -> config.getSomething().setFoo(12)) + ... +``` + +Or in setup object: + +```java +@EnableSetup +static TestEnvironmentSetup setup = ext -> + ext.configModifiers(config -> config.getSomething().setFoo(12)) +``` + +Modifier could be declared in class: + +```java +public class MyModifier implements ConfigModifier { + @Override + public void modify(MyConfig config) throws Exception { + config.getSomething().setFoo(12); + } +} + +@TestGuiceyApp(.., configModifiers = MyModifier.class) +``` + +!!! tip + Modifier could be used with both manual configuration or usual (yaml) configuration. + Configuration modifiers also could be used together with configuration overrides. + +!!! warning "Limitation" + Configuration modifiers are called after dropwizard logging configuration, + so logging is the only thing that can't be configured (use configuration overrides for logging) + +## Deferred configuration + +If you need to configure value, supplied by some other extension, or value may be resolved only +after test start, then static overrides declaration is not an option. In this case use +[alternative extensions declaration](run.md#alternative-declaration) which provides additional +config override methods: + +```java +@RegisterExtension +@Order(1) +static FooExtension ext = new FooExtension(); + +@RegisterExtension +@Order(2) +static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 1") + .configOverride("bar", () -> ext.getValue()) + .configOverrides(new ConfigOverrideValue("baa", () -> "44")) + .create(); +``` + +In most cases `configOverride("bar", () -> ext.getValue())` would be enough to configure a supplier instead +of static value. + +In more complex cases, you can use custom implementations of `ConfigOverride`. + +!!! warning "" + Guicey have to accept only `ConfigOverride` objects implementing custom + `ru.vyarus.dropwizard.guice.test.util.ConfigurablePrefix` interface. + In order to support parallel tests guicey generates unique config prefix for each test + (because all overrides eventually stored to system properties) and so it needs a way + to set this prefix into custom `ConfigOverride` objects. + +## Configuration from 3rd party extensions + +If you have junit extension (e.g. which starts db for test) and you need +to apply configuration overrides from that extension, then you should simply +store required values inside junit storage: + +```java +public class ConfigExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + // do something and then store value + context.getStore(ExtensionContext.Namespace.GLOBAL).put("ext1", 10); + } +} +``` + +And map overrides directly from store using `configOverrideByExtension` method: + +```java +@ExtendWith(ConfigExtension.class) +public class SampleTest { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) + .configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1") + .create(); +} +``` + +Here, value applied by extension under key `ext1` would be applied to configuration `ext1` path. +If you need to use different configuration key: + +```java +.configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1", "key") +``` + +!!! tip + You can use [setup objects](setup-object.md) instead of custom junit extensions for test environment setup + + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/debug.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/debug.md new file mode 100644 index 000000000..787135110 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/debug.md @@ -0,0 +1,182 @@ +# Debug + +All declared setup objects and hooks could be listed with a (declaration) source reference (where possible) +in initialization order. + +```java +public static class Test2 extends Base { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) + .setup(Ext1.class, Ext2.class) + .setup(it -> null, new Ext3()) + .debug() + .create(); + + @EnableSetup + static TestEnvironmentSetup ext1 = it -> null; + @EnableSetup + static TestEnvironmentSetup ext2 = it -> null; +``` + +``` +Guicey test extensions (Test2.): + + Setup objects = + Ext1 @RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:102) + Ext2 @RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:102) + @RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:103) + Ext3 @RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:103) + @EnableSetup Base#base1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base1 + @EnableSetup Base#base2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base2 + @EnableSetup Test2#ext1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext1 + @EnableSetup Test2#ext2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext2 +``` + +Also, applied configuration overrides and modifiers would be shown: + +``` +Configuration overrides (Test2.): + foo = 2 + bar = 11 + +Configuration modifiers: + @RegisterExtension.configModifiers(obj) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:100) + CfgModify1 @RegisterExtension.configModifiers(class) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:101) + @EnableSetup Test2#setup.configModifiers(obj) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:107) + CfgModify2 @EnableSetup Test2#setup.configModifiers(class) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:108) +``` + +!!! important + Configuration overrides printed **after** application startup because they are + extracted from system properties (to guarantee exact used value), which is possible + to analyze only after `DropwizardTestSupport#before()` call. + +!!! note + Configuration prefix for system properties is shown in brackets: `(Test1.)`. + It simplifies investigation in case of concurrent tests. + +Debug could be activated by annotation: + +```java +@TestGuiceyApp(value = App.class, debug = true) +``` + +By builder: + +```java +@RegisterExtension +TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App) + .debug() + .create() +``` + +By setup object: + +```java +@EnableSetup +static TestEnvironmentSetup db = ext -> { + ext.debug(); + }; +``` + +And using system property: + +``` +-Dguicey.extensions.debug=true +``` + +There is also a shortcut for enabling system property: + +```java +TestSupport.debugExtensions() +``` + +## Startup performance + +To simplify slow tests (slowness) investigations, guicey measures and prints extensions time. + +For example, test with application started in beforeAll, with two test methods +(same app for both tests): + +```java +@TestGuiceyApp(value = App.class, debug = true) +public class PerformanceLogTest { + @Test + public void test1() { ... } + @Test + public void test2() { ... } +} +``` + +``` +\\\------------------------------------------------------------/ test instance = 1595d2b2 / +Guicey time after [Before each] of PerformanceLogTest#test1(): 1204 ms + + [Before all] : 1204 ms + Guicey fields search : 2.03 ms + Guicey hooks registration : 0.02 ms + Guicey setup objects execution : 1.92 ms + DropwizardTestSupport creation : 1.47 ms + Application start : 1172 ms + + [Before each] : 0.46 ms + Guice fields injection : 0.19 ms + + +\\\------------------------------------------------------------/ test instance = 45554613 / +Guicey time after [Before each] of PerformanceLogTest#test2(): 1205 ms ( + 0.33 ms) + + [Before each] : 0.69 ms ( + 0.23 ms) + Guice fields injection : 0.36 ms ( + 0.17 ms) + + [After each] : 0.10 ms + + +\\\--------------------------------------------------------------------------------------------- +Guicey time after [After all] of PerformanceLogTest: 1207 ms ( + 2.15 ms) + + [After each] : 0.11 ms ( + 0.01 ms) + + [After all] : 2.14 ms + Application stop : 1.72 ms +``` + +There are three reports: + +1. Before first test method (see guicey extension startup time) +2. Before the second test method (see guicey time for the second method only) +3. After all (cleanup time) + +Only the first report shows all recorded times, next reports only mention time increase. +For example, the second report mentions only `Guice fields injection : 0.36 ms ( + 0.17 ms)` +Meaning guicey perform fields injection just before the second test, spent 0.17 ms on it +(overall injection time for two injections is 0.36 ms) + + +## Extensions + +It is recommended to use root extension debug option value in the [extensions](setup-object.md). +Current field-bases extensions print recognized fields report when debug is enabled. + + +```java +@TestGuiceyApp(value = App.class, stup = MySetup.class, debug = true) + +public class MySetup implements TestEnvironmentSetup, TestExecutionListener { + @Override + public Object setup(TestExtension extension) throws Exception { + extension.listen(this); + + if (extension.isDebug()) { + System.out.println("Debug info: ..."); + } + } + + @Override + public void started(final EventContext context) throws Exception { + if (context.isDebug()) { + System.out.println("Debug info: ..."); + } + } +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/env.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/env.md new file mode 100644 index 000000000..03675e994 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/env.md @@ -0,0 +1,43 @@ +# Environment variables + +!!! warning + Such modifications are not suitable for parallel tests execution! + + Use [`@Isolated`](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization) + on such tests to prevent parallel execution with other tests + +To modify environment variables for test use [system stubs](https://github.com/webcompere/system-stubs) library + +```groovy +testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.3' +testImplementation 'org.objenesis:objenesis:3.3' +``` + +```java +@ExtendWith(SystemStubsExtension.class) +public class MyTest { + @SystemStub + EnvironmentVariables ENV; + @SystemStub + SystemOut out; + @SystemStub + SystemProperties propsReset; + + @BeforeAll + public void setup() { + ENV.set("VAR", "1"); + System.setProperty("foo", "bar"); // OR propsReset.set("foo", "bar") - both works the same + } + + @Test + public void test() { + // here goes some test that requires custom environment and system property values + + // validating output + Assertions.assertTrue(out.getTest().contains("some log message")); + } +} +``` + +Pay attention that there is no need for cleanup: system properties and environment variables would be re-set automatically! + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/hooks.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/hooks.md new file mode 100644 index 000000000..ff5337cdd --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/hooks.md @@ -0,0 +1,118 @@ +# Application modification + +You can use [hooks to customize application](../overview.md#configuration-hooks). + +!!! note + Hook provides the same methods as the main `GuiceBundle.builder()` and + so it could easily re-configure application (change options, add or disable modules, + enable reports, etc.) + +In both extension annotations, hooks could be declared with attribute: + +```java +@TestDropwizardApp(value = MyApplication.class, hooks = MyHook.class) +``` + +or + +```java +@TestGuiceyApp(value = MyApplication.class, hooks = MyHook.class) +``` + +Where MyHook is: + +```java +public class MyHook implements GuiceyConfigurationHook { + @Override + public void configure(GuiceBundle.Builder builder) throws Exception { + + } +} +``` + +Many test extensions could be written with hooks. For example, to implement deep mocks +support we can write hook like this: + +```java +public class MockApiHook implements GuiceyConfigurationHook { + private final Class[] classes; + + public MockApiHook(final Class... classes) { + this.classes = classes; + } + + @Override + public void configure(final GuiceBundle.Builder builder) { + builder.modulesOverride(binder -> { + for (Class clazz : classes) { + bind(binder, clazz, Mockito.mock(clazz)); + } + }); + } +} +``` + +Usage: + +```java +// mocks created and registered as overriding original services +@EnableHook +static MockApiHook mocks = new MockApiHook(SomeService.class, SomeOtherService.class); + +@Inject +SomeService service; + +@BeforeEach +public void setUp() { + // In case of multiple test methods Mockito.reset(service) required + // Would work correctly only if AOP not used for service + Mockito.when(service.getFoo()).thenReturn("12"); +} + +@test +public void test() { + Assertions.assertEquals("12", service.getFoo()); +} +``` + +!!! important + This is just a simple example (not counting possible AOP usage) - just to show how hooks + could be used. Mocks support is already implemented: see `@MockBean` extension. + +## Hook fields + +Alternatively, you can declare hook directly in test field: + +```java +@EnableHook +static GuiceyConfigurationHook HOOK = builder -> builder.modules(new DebugModule()); +``` + +!!! tip + Hook field could be used for guicey report activation in test: + ```java + @EnableHook + static GuiceyConfigurationHook hook = GuiceBundle.Builder::printStartupTime; + ``` + +Any number of hook fields could be declared. +Hook fields could be also declared in base test class: + +```java +public abstract class BaseTest { + + // hook in base class + @EnableHook + static GuiceyConfigurationHook BASE_HOOK = builder -> builder.modules(new DebugModule()); +} + +@TestGuiceyApp(value = App.class, hooks = SomeOtherHook.class) +public class SomeTest extends BaseTest { + + // Another hook + @EnableHook + static GuiceyConfigurationHook HOOK = builder -> builder.modules(new DebugModule2()); +} +``` + +All 3 hooks will work (two in fields, one in annotation). \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/inject.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/inject.md new file mode 100644 index 000000000..dc109ac95 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/inject.md @@ -0,0 +1,187 @@ +# Guice injections + +Any guice bean could be injected directly into a test field: + +```groovy +@Inject +SomeBean bean +``` + +This will work even for not declared (in guice modules) beans (JIT injection will occur). + +To better understand injection scopes look the following test: + +```groovy +// one application instance started for all test methods +@TestGuiceyApp(AutoScanApplication.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class InjectionScopeTest { + + // new instance injected on each test + @Inject + TestBean bean; + + // the same context used for all tests (in class), so the same bean instance inserted before each test + @Inject + TestSingletonBean singletonBean; + + @Test + @Order(1) + public void testInjection() { + bean.value = 5; + singletonBean.value = 15; + + Assertions.assertEquals(5, bean.value); + Assertions.assertEquals(15, singletonBean.value); + + } + + @Test + @Order(2) + public void testSharedState() { + + Assertions.assertEquals(0, bean.value); + Assertions.assertEquals(15, singletonBean.value); + } + + // bean is in prototype scope + public static class TestBean { + int value; + } + + @Singleton + public static class TestSingletonBean { + int value; + } +} +``` + + +!!! note + Guice AOP *will not work* on test methods (because test instances are not created by guice). + +## Parameter injection + +Any **declared** guice bean may be injected as test method parameter: + +```java +@Test +public void testSomthing(DummyBean bean) +``` + +(where `DummyBean` is manually declared in some module or requested as a dependency +(JIT-instantiated) during injector creation). + +For unknown beans injection (not declared and not used during startup) special annotation must be used: + +```java +@Test +public void testSomthing(@Jit TestBean bean) +``` + +!!! info + Additional annotation required because you may use other junit extensions providing their own + parameters, which guicey extension should not try to handle. That's why not annotated parameters + verified with existing injector bindings. + +Qualified and generified injections will also work: + +```java +@Test +public void testSomthing(@Named("qual") SomeBean bean, + TestBean generifiedBean, + Provider provider) +``` + +Also, there are special objects available as parameters: + +* `Application` or exact application class (`MyApplication`) +* `ObjectMapper` +* `ClientSupport` application web client helper +* `DropwizardTestSupport` test support object used internally +* `ExtensionContext` junit extension context + +!!! note + Parameter injection will work on test methods as well as lifecyle methods (beforeAll, afterEach etc.) + +Example: + +```java +@TestDropwizardApp(AutoScanApplication.class) +public class ParametersInjectionDwTest { + + public ParametersInjectionDwTest(Environment env, DummyService service) { + Preconditions.checkNotNull(env); + Preconditions.checkNotNull(service); + } + + @BeforeAll + static void before(Application app, DummyService service) { + Preconditions.checkNotNull(app); + Preconditions.checkNotNull(service); + } + + @BeforeEach + void setUp(Application app, DummyService service) { + Preconditions.checkNotNull(app); + Preconditions.checkNotNull(service); + } + + @AfterEach + void tearDown(Application app, DummyService service) { + Preconditions.checkNotNull(app); + Preconditions.checkNotNull(service); + } + + @AfterAll + static void after(Application app, DummyService service) { + Preconditions.checkNotNull(app); + Preconditions.checkNotNull(service); + } + + @Test + void checkAllPossibleParams(Application app, + AutoScanApplication app2, + Configuration conf, + TestConfiguration conf2, + Environment env, + ObjectMapper mapper, + Injector injector, + ClientSupport client, + DropwizardTestSupport support, + DummyService service, + @Jit JitService jit) { + assertNotNull(app); + assertNotNull(app2); + assertNotNull(conf); + assertNotNull(conf2); + assertNotNull(env); + assertNotNull(mapper); + assertNotNull(injector); + assertNotNull(client); + assertNotNull(support); + assertNotNull(service); + assertNotNull(jit); + assertEquals(client.getPort(), 8080); + assertEquals(client.getAdminPort(), 8081); + } + + public static class JitService { + + private final DummyService service; + + @Inject + public JitService(DummyService service) { + this.service = service; + } + } +} +``` + +!!! tip + `DropwizardTestSupport` and `ClientSupport` objects are also available with a static calls (in the same thread): + + ```java + DropwizardTestSupport support = TestSupport.getContext(); + ClientSupport client = TestSupport.getContextClient(); + ``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/junit-ext.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/junit-ext.md new file mode 100644 index 000000000..40ccaff39 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/junit-ext.md @@ -0,0 +1,82 @@ +# 3rd party extensions integration + +It is extremely simple in JUnit 5 to [write extensions](https://junit.org/junit5/docs/current/user-guide/#extensions). +If you do your own extension, you can easily integrate with guicey or dropwizard extensions. + +!!! tip + In many cases, it would be easier to write a custom guicey [setup object](setup-object.md) + which provides almost the same abilities as junit extensions plus guicey awareness. + All field-based extensions in guicey are implemented with setup objects. + +## Guicey side + +If you already have a junit extension that stores something in `ExtensionContext` then you can: + +1. Bind value into [configuration directly](config.md#configuration-from-3rd-party-extensions): + `.configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext-key", "config.key")` +2. Bind junit `ExtensionContext` as test method parameter (and access storage manually): +```java +@BeforeAll +public static void beforeAll(ExtensionContext junitContext) { + ... +} +``` +3. Inside [setup object](setup-object.md) access junit context: +```java +public class MyExt implements GuiceyEnvironmentSetup { + @Override + public Object setup(TestExtension extension) throws Exception { + ExtensionContext context = extension.getJunitContext(); + } +} +``` + +## Extension side + +There are special static methods allowing you to obtain main test objects: + +* `GuiceyExtensionsSupport.lookupSupport(extensionContext)` -> `Optional` +* `GuiceyExtensionsSupport.lookupInjector(extensionContext)` -> `Optional` +* `GuiceyExtensionsSupport.lookupClient(extensionContext)` -> `Optional` + +For example: + +```java +public class MyExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + Injector injector = GuiceyExtensionsSupport.lookupInjector(context).get(); + ... + } +} +``` + +(guicey holds test state in junit test-specific storages and that's why test context is required) + +!!! warning + There is no way in junit to order extensions, so you will have to make sure that your extension + will be declared after guicey extension (`@TestGuiceyApp` or `@TestDropwizardApp`). + +There is intentionally no direct api for applying configuration overrides from +3rd party extensions because it would be not obvious. Instead, you should always +declare overridden value in extension declaration. Either use instance getter: + +```java +@RegisterExtension +static MyExtension ext = new MyExtension() + +@RegisterExtension +static TestGuiceyAppExtension dw = TestGuiceyAppExtension.forApp(App.class) + .configOverride("some.key", ()-> ext.getValue()) + .create() +``` + +Or store value [inside junit store](#configuration-from-3rd-party-extensions) and then reference it: + +```java +@RegisterExtension +static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) + .configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1") + .create(); +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/logs.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/logs.md new file mode 100644 index 000000000..8a0d05045 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/logs.md @@ -0,0 +1,195 @@ +# Testing logs + +!!! important + Works only with logback (default dropwizard logger). + +`@RecordLogs` extension could record log messages for one or multiple classes. + +For example, for service: + +```java +public class Service { + private final Logger logger = LoggerFactory.getLogger(Service.class); + + public void foo() { + ... + if (someCondition) { + logger.debug("Some technical note"); + } + } +} +``` + +Testing log appears: + +```java +@Isolated +@TestGucieyApp(App.class) +public class Test { + + @RecordLogs(value = Service.class, level = Level.DEBUG) + RecordedLogs logs; + + @Inject + Service service; + + @Test + public void test() { + // here some actions with service, involving logging + service.foo(); + + Assertions.assertEquals(1, logs.count()); + Assertions.assertTrue(logs.has(Level.DEBUG)); + Assertions.assertEquals(1, logs.containing("Some technical note").count()); + // alternative: last message was a technical hint + Assertions.assertEquals("Some technical note", logs.lastMessage()); + } +} +``` + +Logs could be collected for any custom logger name or entire package: + +```java +@RecordLogs(loggers = "ru.vyarus.dropwizard.guice.test", level = Level.TRACE) +RecordedLogs logs; +``` + +!!! warning + Such tests could not be run in parallel because logger configuration is global + (use `@Isolated` to prevent parallel execution) + +## Registration + +You can register as many recorders as you like. Each recorder could listen one or more +loggers. + +To listen all warnings (root logger): + +```java +@RecordLogs(level = Level.WARN) +RecordedLogs logs; +``` + +To listen all loggers in package: + +```java +@RecordLogs(loggers = "com.my.package", level = Level.WARN) +RecordedLogs logs; +``` + +To listen exact class and package: + +```java +@RecordLogs(value = SomeClass.class, loggers = "com.my.package", level = Level.INFO) +RecordedLogs logs; +``` + +## Implementation details + +Each recorder registration leads to logging appender registration for a target logger +(or multiple loggers). + +If the current logger configuration is higher than required, then **logger would be +re-configured**. For example, if default logger level is `INFO` and recorder requires `TRACE` messages, +then it would change logger configuration to receive required messages. + +!!! tip + Recorder might be used just to enable required logs, without application + logging configuration. This is very useful in tests (to enable `DEBUG` or `TRACE` + messages for exact service (or package)): + ```java + @RecordLogs(value = MyClass.class, level = Level.TRACE) + RecordedLogs logs; + ``` + +During application startup **dropwizard resets loggers two times** and hook would +re-attach appenders to compensate it. You should be able to record all messages from application startup, +except logs from dropwizard bundles, registered BEFORE `GuiceBundle`. + +## Querying + +`RecordedLogs` used to query recorded logs. Root object always contains all recorded events +(for configured loggers). + +Recorded logs are accessible in form of raw *event* (`ILoggingEvent`) or pure string *message* +(formatted messages with arguments). + +| Method | Description | Example | +|-------------------|-------------------------------------------------|---------------------------------------------------------------------------| +| `count()` | Recorded logs count | `assertEquals(1, logs.count())` | +| `empty()` | Events recorded | `assertFalse(logs.empty())` | +| `events()` | All recorded events | `List events = logs.events()` | +| `messages()` | Messages of all recorded messages | `List messages = logs.messages()` | +| `has(loggerName)` | Checks if messages from target logger available | `assertTrue(logs.has(Service.class))`, `assertTrue(logs.has("com.some"))` | +| `has(level)` | Checks if messages of level available | `assertTrue(logs.has(Level.WARN))` | +| `lastEvent()` | Last recorded event or null | `assertEquals(Level.WARN, logs.lastEvent().getLevel())` | +| `lastMessage()` | Message of the last recorded event or null | `assertEquals("Something", logs.lastMessage())` | + +Also, logs could be filtered: + +| Filter | Description | Example | +|----------------------|------------------------------------------------|--------------------------------------------------------------| +| `level(level)` | Select events with level | `logs.level(Level.WARN)` | +| `logger(loggerName)` | Select events of required loggers | `logs.logger(Service.class)`, `logs.logger("com.some")` | +| `containing(String)` | Events where messages contains provided string | `logs.containing("Substring")` | +| `matching(regex)` | Events where messages match provided regex | `logs.matching("something \\d+")` | +| `select(predicate)` | General events matching predicate | `logs.select(event -> event.getLevel().equals(Level.TRACE))` | + +Filters return another matcher object where all verification and filter methods above could be called +(multiple filters could be applied consequently). + +For example, verify count of all messages containing string: + +```java +assertEquals(1, logs.containing("Something").count()); +``` + +Or filtering by logger and level (if recorder records multiple loggers): + +```java +assertEquals(12, logs.logger("com.some.package").level(Level.WARN).count()) +``` + + +## Clear recordings + +Recorded logs could be cleared at any time (to simplify exact method logs matching): + +```java +// clear logs, recorded during application startup +logs.clear(); +// call method +service.foo(); +// verify logs appeared during method call +assertEquals(1, logs.containing("Something").count()); + +// clear again to check logs of another method +logs.clear(); +service.boo(); +... +``` + +By default, recorded logs cleared after each test method. +This could be disabled with `autoReset` option: + +```java +@RecordLogs(value = MyClass.class, level = Level.INFO, autoReset = false) +RecordedLogs logs; +``` + +## Debug + +When extension debug is active: + +```java +@TestGucieyApp(value = App.class, debug = true) +public class Test +``` + +All recognized log recorder fields would be logged: + +``` +Applied log recorders (@RecordLogs) on Test + + #logs DEBUG Service +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/mocks.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/mocks.md new file mode 100644 index 000000000..875301abf --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/mocks.md @@ -0,0 +1,290 @@ +# Testing with mocks + +[Mockito](https://site.mockito.org/) mocks are essentially an automatic [stubs](stubs.md): +with the ability to dynamically declare method behavior (by default, all mock methods +return default value: often null). + +Mocks declared with a `@MockBean` annotation. + +!!! warning + Stubs will not work for HK2 beans + +Mockito documentation is written in the `Mockito` class [javadoc](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html). +Additional docs could be found in [mockito wiki](https://github.com/mockito/mockito/wiki/FAQ) +Also, see official [mockito refcard](https://dzone.com/refcardz/mockito) +and [baeldung guides](https://www.baeldung.com/mockito-series). + +## Setup + +Requires mockito dependency (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +!!! note "Remember" + * Do not mock types you don’t own + * Don’t mock value objects + * Don’t mock everything + * Show love with your tests! + + [source](https://site.mockito.org/#more), [explanations](https://github.com/mockito/mockito/wiki/How-to-write-good-tests) + +For example, suppose we have a service: + +```java +public class Service { + public String foo() { + ... + } +} +``` + +where method foo implements some complex logic, not required in test. + +To override service with a mock: + +```java +@TestGuiceyApp(App.class) +public class Test { + + // register mock (mock would be created automatically using Mockito.mock(Service.class) + @MockBean + Service mock; + + // injecting here to show that mock replaced real service + @Inject + Service service; + + @BeforeEach + public void setUp() { + // declaring behaviour + when(mock.foo()).thenReturn("static value"); + } + + @Test + public void test() { + // mock instance instead of service + Assertions.assertEquals(mock, service); + // method overridden + Assertions.assertEquals("static value", service.foo()); + } +} +``` + +Here `when` refer to `Mockito.when()` used with static import. + +!!! important + Guice AOP would not be applied to mocks (only guice-managed beans support AOP) + +You can also provide a pre-created mock instance (useful if mock used during application startup or partial mocks): + +```java +@MockBean +static Service mock = createMock(); +``` + +!!! note + When mock is registered with instance, mock field must be static for per-test application run + (default annotation). It may not be static for per-method application startup (with `@RegisterExtension`). + +## Mocking examples + +Mocking answers for different arguments: + +```java +when(mock.foo(10)).thenReturn(100); +when(mock.foo(20)).thenReturn(200); +when(mock.foo(30)).thenReturn(300); +``` + +Different method answers (for consequent calls): + +```java +when(mock.foo(anyInt())).thenReturn(10, 20, 30); +``` + +Using actual argument in mock: + +```java + when(mock.getValue(anyInt())).thenAnswer(invocation -> { + int argument = (int) invocation.getArguments()[0]; + int result; + switch (argument) { + case 10: + result = 100; + break; + case 20: + result = 200; + break; + case 30: + result = 300; + break; + default: + result = 0; + } + return result; + }); +``` + +## Asserting calls + +Mock could also be used for calls verification: + +```java +// method Service.foo() called on mock just once +verify(mock, times(1)).foo(); +// method Service.bar(12) called just once (with exact argument value) +verify(mock, times(1)).bar(12); +``` + +These assertions would fail if method was called more times or using different arguments. + +## Mock reset + +Mocks are re-set automatically after each test method (and that's why it makes +sense to declare mock behavior in test setup method - execured before each test method). + +!!! note + Mock could be reset manually at any time with `Mockito.reset(mock)` + +Mocks automatic reset could be disabled with `autoReset` option: + +```java +@MockBean(autoReset = false) +Service mock; +``` + +## Partial mocks + +If mock is applied for a class with implemented methods, these methods would +still be overridden with fake implementations. If you want to preserve this logic, then +use spies: + +```java +public class AbstractService implements IService { + public abstract String bar(); + + public String foo() { + return "value"; + } +} + +@TestGuiceyApp(App.class) +public class Test { + + @MockBean + static IService mock = Mockito.spy(AbstractService.class); + + @Inject + IService service; + + @Test + public void test() { + // default mock implementation for abstract method + Assertions.assertNull(service.bar()); + // implemented method preserved + Assertions.assertEquals("value", service.foo()); + } +} +``` + +!!! note + The [spies](spies.md) section covers only spies, spying on real guice bean instance. + Using spies for partial mocks is more related to pure mocking and so it's described here. + +## Mocks report + +Mockito provides a mock usage report (`Mockito.mockingDetails(value).printInvocations()`), +which could be enabled with `@MockBean(printSummary = true)` (report shown after each test method): + +``` +\\\------------------------------------------------------------/ test instance = 6d420cdd / +@MockBean stats on [After each] for MockSummaryTest$Test1#test(): + + [Mockito] Interactions of: Mock for Service, hashCode: 1340267778 + 1. service.foo(1); + -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.test(MockSummaryTest.java:55) + - stubbed -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.setUp(MockSummaryTest.java:50) +``` + +## Debug + +When extension debug is active: + +```java +@TestGucieyApp(value = App.class, debug = true) +public class Test +``` + +All recognized mock fields would be logged: + +``` +Applied mocks (@MockBean) on MockSimpleTest: + + #mock2 Service2 (r.v.d.g.t.j.s.m.MockSimpleTest) AUTO + #mock1 Service1 (r.v.d.g.t.j.s.m.MockSimpleTest) AUTO +``` + +## Mocking OpenAPI client + +If you use some external API with a client, generated from openapi (swagger) declaration, +then you should be using it in code like this: + +```java + +@Inject +SomeApi api; + +public void foo() { + Some response = api.someGetCall(...) +} +``` + +Where `SomeApi` is a generated client class. + +Usually, the simplest way is to record real service response (using swagger UI or other generated documentation) +or simply enabling client debug in the application (so all requests and responses would be logged). + +Store such responses as json files in test resources: e.g. `src/test/resources/responses/someGet.json` + +Now mocking `SomeApi` and configure it to return object, mapped from json file content, instead of the real call: + +```java +@TestGuiceyApp(App.class) +public class Test { + @MockBean + SomeApi mock; + + // injecting here to show that mock replaced real service + @Inject + SomeService service; + + @Inject + Environment environment; + + @BeforeEach + public void setUp() throws Exception { + // usually better than new ObjectMapper() (already pre-configured with extensions) + ObjectMapper mapper = environment.getObjectMapper(); + when(mock.someGetCall(...)).thenReturn(mapper.readValue( + new File("src/test/resources/responses/someGet.json"), Some.class)); + } + + @Test + public void test() { + // call some service using api internally (mock removes external call) + service.doSomething(); + } +} +``` + +With it, object, mapped from json file, would be returned on service call, instead of +the real api. + +!!! note + In the example, direct file access used instead of classpath lookup because + IDEA by default does not copy `.json` resources (it must be additionally configured) and + so direct file access is more universal. \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/nested.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/nested.md new file mode 100644 index 000000000..cc88c789c --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/nested.md @@ -0,0 +1,139 @@ +# Junit nested tests + +Junit natively supports [nested tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested). + +Guicey extensions affects all nested tests below declaration (nesting level is not limited): + +```java +@TestGuiceyApp(AutoScanApplication.class) +public class NestedPropagationTest { + + @Inject + Environment environment; + + @Test + void checkInjection() { + Assertions.assertNotNull(environment); + } + + @Nested + class Inner { + + @Inject + Environment env; // intentionally different name + + @Test + void checkInjection() { + Assertions.assertNotNull(env); + } + } +} +``` + +!!! note + Nested tests will use exactly the same guice context as root test (application started only once). + +Extension declared on nested test will affect all sub-tests: + +```java +public class NestedTreeTest { + + @TestGuiceyApp(AutoScanApplication.class) + @Nested + class Level1 { + + @Inject + Environment environment; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(environment); + } + + @Nested + class Level2 { + @Inject + Environment env; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(env); + } + + @Nested + class Level3 { + + @Inject + Environment envr; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(envr); + } + } + } + } + + @Nested + class NotAffected { + @Inject + Environment environment; + + @Test + void extensionNotApplied() { + Assertions.assertNull(environment); + } + } +} +``` + +This way nested tests allows you to use different extension configurations in one (root) class. + +Note that extension declaration with `@RegisterExtension` on the root class field would also +be applied to nested tests. Even declaration in non-static field (start application for each method) +would also work. + +## Use interfaces to share tests + +This is just a tip on how to execute same test method in different environments. + +```java +public class ClientSupportDwTest { + + interface ClientCallTest { + // test to apply for multiple environments + @Test + default void callClient(ClientSupport client) { + Assertions.assertEquals("main", client.targetApp("servlet") + .request().buildGet().invoke().readEntity(String.class)); + } + } + + @TestDropwizardApp(App.class) + @Nested + class DefaultConfig implements ClientCallTest { + + @Test + void testClient(ClientSupport client) { + Assertions.assertEquals("http://localhost:8080/", client.basePathApp()); + } + } + + @TestDropwizardApp(value = App.class, configOverride = { + "server.applicationContextPath: /app", + "server.adminContextPath: /admin", + }, restMapping = "api") + @Nested + class ChangedDefaultConfig implements ClientCallTest { + + @Test + void testClient(ClientSupport client) { + Assertions.assertEquals("http://localhost:8080/app/", client.basePathApp()); + } + } +} +``` + +Here test declared in `ClientCallTest` interface will be called for each nested test +(one declaration - two executions in different environments). + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/output.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/output.md new file mode 100644 index 000000000..9eda516d8 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/output.md @@ -0,0 +1,30 @@ +# Testing console output + +There is a utility to capture console output: + +```java +@Isolated +@TestWebApp(App.class) +public class Test { + + @Test + public void testRestCall(ClientSupport client) { + String out = TestSupport.captureOutput(() -> { + // call application api endpoint + client.get("sample/get", null); + }); + + // uses assert4j, test that client was called (just an example) + Assertions.assertThat(out) + .contains("[Client action]---------------------------------------------{"); + } +} +``` + +Returned output contains both `System.out` and `System.err` - same as it would be seen in console. + +All output is also printed into console to simplify visual validation + +!!! warning + Such tests could not be run in parallel (due to system io overrides) and so should be + annotated with `@Isolated` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/parallel.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/parallel.md new file mode 100644 index 000000000..f048ef818 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/parallel.md @@ -0,0 +1,15 @@ +## Parallel execution + +Junit [parallel tests execution](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution) +could be activated with properties file `junit-platform.properties` located at test resources root: + +```properties +junit.jupiter.execution.parallel.enabled = true +junit.jupiter.execution.parallel.mode.default = concurrent +``` + +!!! note + In order to avoid config overriding collisions (because all overrides eventually stored to system properties) + guicey generates unique property prefixes in each test. + +To avoid port collisions in dropwizard tests use [randomPorts option](#random-ports). \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/rest.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/rest.md new file mode 100644 index 000000000..df9a0dbd3 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/rest.md @@ -0,0 +1,310 @@ +# Testing rest + +Guicey provides lightweight REST testing support: same as [dropwizard resource testing support](https://www.dropwizard.io/en/stable/manual/testing.html#testing-resources), +but with guicey-specific features. + +Such tests would not start web container: all rest calls are simulated (but still, it tests every part of resource execution). + +!!! important + Rest stubs work only with lightweight guicey run (they are simply useless when web container started) + +Lightweight REST could be declared with `@StubRest` annotation. + +```java +@TestGuiceyApp(App.class) +public class Test { + + @StubRest + RestClient rest; + + @Test + public void test() { + String res = rest.get("/foo", String.class); + Assertions.assertEquals("something", res); + + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/error", String.class)); + Assertions.assertEquals("error message", ex.getResponse().readEntity(String.class)); + } +} +``` + +!!! note + Extension naming is not quite correct: it is not a stub, but real application resources are used. + The word "stub" used to highlight the fact of incomplete startup: only rest without web. + +By default, all declared resources would be started with all existing jersey extensions +(filters, exception mappers, etc.). **Servlets and http filters are not started** +(guicey disables all web extensions to avoid their (confusing) appearance in console) + +## Selecting resources + +Real tests usually require just one resource (to be tested): + +```java +@StubRest(MyResource.class) +RestClient rest; +``` + +This way only one resource would be started (and all resources directly registered in +application, not as guicey extension). All jersey extensions will remain. + +Or a couple of resources: + +```java +@StubRest({MyResource.class, MyResource2.class}) +RestClient rest; +``` + +Or you may disable some resources: + +```java +@StubRest(disableResources = {MyResource2.class, MyResource3.class}) +RestClient rest; +``` + +## Disabling jersey extensions + +Often jersey extensions, required for the final application, make complications for testing. + +For example, exception mapper: dropwizard register default exception mapper which +returns only the error message, instead of actual exception (and so sometimes we can't check the real cause). + +`disableDropwizardExceptionMappers = true` disables extensions, registered by dropwizard. + +When default exception mapper enabled, resource throwing runtime error would return just error code: + +```java +@Path("/some/") +@Produces("application/json") +public class ErrorResource { + + @GET + @Path("/error") + public String get() { + throw new IllegalStateException("error"); + } +} +``` + +```java +@TestGuiceyApp +public class Test { + + @StubRest + RestClient rest; + + public void test() { + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/some/error", String.class)); + + // exception hidden, only generic error code + Assertions.assertTrue(ex.getResponse().readEntity(String.class) + .startsWith("{\"code\":500,\"message\":\"There was an error processing your request. It has been logged")); + } +} +``` + +Without dropwizard exception mapper, we can verify exact exception: + +```java +public class Test { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest; + + public void test() { + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.get("/error", String.class)); + // exception available + Assertions.assertTrue(ex.getCause() instanceof IllegalStateException); + } +} +``` + +It might be useful to disable application extensions also with `disableAllJerseyExtensions`: + +```java +```java +@StubRest(disableDropwizardExceptionMappers = true, + disableAllJerseyExtensions = true) +RestClient rest; +``` + +This way raw resource would be called without any additional logic. + +!!! note + Only extensions, managed by guicey could be disabled: extensions directly registered + in dropwizard would remain. + +Also, you can select exact extensions to use (e.g., to test it): + +```java +@StubRest(jerseyExtensions = CustomExceptionMapper.class) +RestClient rest; +``` + +Or disable only some extensions (for example, disabling extension implementing security): + +```java +@StubRest(disableJerseyExtensions = CustomSecurityFilter.class) +RestClient rest; +``` + +## Debug + +Use **debug** output to see what extensions were actually included and what disabled: + +```java +@TestGuiceyApp(.., debug = true) +public class Test { + @StubRest(disableDropwizardExceptionMappers = true, + disableResources = Resource2.class, + disableJerseyExtensions = RestFilter2.class) + RestClient rest; +} +``` + +``` +REST stub (@StubRest) started on DebugReportTest$Test1: + + Jersey test container factory: org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory + Dropwizard exception mappers: DISABLED + + 2 resources (disabled 1): + ErrorResource (r.v.d.g.t.j.s.r.support) + Resource1 (r.v.d.g.t.j.s.r.support) + + 2 jersey extensions (disabled 1): + RestExceptionMapper (r.v.d.g.t.j.s.r.support) + RestFilter1 (r.v.d.g.t.j.s.r.support) + + Use .printJerseyConfig() report to see ALL registered jersey extensions (including dropwizard) +``` + +## Requests logging + +By default, rest client would log requests and responses: + +```java +@TestGuiceyApp(App.class) +public class Test { + + @StubRest + RestClient rest; + + @Test + public void test() { + String res = rest.get("/foo", String.class); + Assertions.assertEquals("something", res); + } +} +``` + +``` +[Client action]---------------------------------------------{ +1 * Sending client request on thread main +1 > GET http://localhost:0/foo + +}---------------------------------------------------------- + + +[Client action]---------------------------------------------{ +1 * Client response received on thread main +1 < 200 +1 < Content-Length: 3 +1 < Content-Type: application/json +something + +}---------------------------------------------------------- +``` + +Logging could be disabled with `logRequests` option: ` @StubRest(logRequests = false)` + +## Container + +By default, [InMemoryTestContainerFactory](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/test-framework.html#d0e18552) +used. + + In-Memory container is not a real container. It starts Jersey application and + directly calls internal APIs to handle request created by client provided by + test framework. There is no network communication involved. This containers + does not support servlet and other container dependent features, but it is a + perfect choice for simple unit tests. + +If it is not enough (in-memory container does not support all functions), then +use `GrizzlyTestContainerFactory` + + The GrizzlyTestContainerFactory creates a container that can run as a light-weight, + plain HTTP container. Almost all Jersey tests are using Grizzly HTTP test container + factory. + +To activate grizzly container add dependency (version managed by dropwizard BOM): + +```groovy +testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2' +``` + +By default, grizzly would be used if it's available on classpath, otherwise in-memory used. +If you need to force any container type use: + +```java +// use in-memory container, even if grizly available in classpath +// (use to force more lightweight container, even if some tests require grizzly) +@StubRest(container = TestContainerPolicy.IN_MEMORY) +``` + +```java +// throw error if grizzly container not available in classpath +// (use to avoid accidental in-memory use) +@StubRest(container = TestContainerPolicy.GRIZZLY) +``` + +## Rest client + +`RestClient` is the same as [ClientSupport#restClient()](client.md), available for guicey extensions. +It extends the same `TestClient` class and so provides the same abilities: + +* [Defaults](client.md#defaults) +* [Shortcut methods](client.md#simple-shortcuts) +* [Builder API](client.md#builder-api) +* [Response assertions](client.md#response-assertions) +* [Static resource client](client.md#resource-clients) +* [Forms builder](client.md#form-builder) + +!!! note + Just in case: `ClientSupport` would not work with rest stubs (because web container is actually + not started and so `ClientSupport` can't recognize a correct rest mapping path). Of course, + it could be used with a full URLs. + +!!! note + Multipart support is enabled automatically when dropwizard-forms available in classpath + + ```java + rest.buildForm("/some/path") + .param("foo", "bar") + .param("file", new File("src/test/resources/test.txt")) + .buildPost() + .asVoid(); + ``` + +To clear defaults: + +```java +rest.reset() +``` + +Might be a part of call chain: + +```java +rest.reset().post(...) +``` + +!!! note + Resource client could be directly injected as a test field + (instead of calling `rest.resourceClient(MyResource.class)`: + + ```java + @WebResourceClient + ResourceClient rest; + ``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/run.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/run.md new file mode 100644 index 000000000..2065dfaec --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/run.md @@ -0,0 +1,280 @@ +# Testing application + +Guicey provides two junit extensions: + +* [@TestGuiceyApp](#testing-core-logic) - for lightweight tests (without starting web part, only guice context) +* [@TestDropwizardApp](#testing-web-logic) - for complete integration tests + +!!! note "" + `@TestGuiceyApp` assumed to be used for the majority of tests as it only starts guice injector + (which is much faster than complete application startup). Such tests are ideal for testing + business logic (services). + + `@TestDropwizardApp` (full integration test) used only to check web endpoints and full workflow + (assuming all business logic was already tested with lightweight tests) + +Both extensions: + +* [Inject guice beans](inject.md) directly in test fields. +* Support [method parameters injection](inject.md#parameter-injection) +* Support [hooks](hooks.md) and [setup objects](setup-object.md) for test configuration +* Support [alternative declaration](#alternative-declaration) for [deferred configuration](#deferred-configuration) + or [starting application for each test method](#start-application-by-test-method). +* Provide pre-configured [http client](client.md) might be used for calling test application endpoints (or external). +* Support junit [parallel execution](#parallel-execution) (no side effects). + + +Field annotations: + +* `@EnableHook` - [hooks](hooks.md#hook-fields) registration +* `@EnableSetup` - [setup objects](setup-object.md#setup-fields) registration +* `@StubBean` - guice bean [stubs](stubs.md) registration +* `@MockBean` - guice bean [mocks](mocks.md) registration (mockito) +* `@SpyBean` - guice bean [spies](spies.md) registration (mockito) +* `@TrackBean` - guice beans [execution tracking](tracks.md) (simpler then mockito spies; suitable for performance testing) +* `@StubRest` - lightweight [REST testing](rest.md) +* `@RecordLogs` - [logs testing](logs.md) + +Method parameter annotations: + +* `@Jit` - for not declared [guice beans injection](inject.md#parameter-injection) + +## Testing core logic + +`@TestGuiceyApp` creates guice injector (runs all application services) without starting jetty (so resources, servlets and filters will not be available). +`Managed` objects will still be handled correctly. + +```java +@TestGuiceyApp(MyApplication.class) +public class AutoScanModeTest { + + @Inject + MyService service; + + @Test + public void testMyService() { + Assertions.assertEquals("hello", service.getSmth()); + } +``` + +Also, [injections](inject.md) work as [method parameters](inject.md#parameter-injection): + +```java +@TestGuiceyApp(MyApplication.class) +public class AutoScanModeTest { + + public void testMyService(MyService service) { + Assertions.assertEquals("hello", service.getSmth()); + } +``` + +Application started before all tests in annotated class and stopped after them. + +### Annotation options + +| Option | Description | Default | +|-----------------|-----------------------------------------------------------------------------|----------------------------| +| config | Configuration file path | "" | +| configOverride | Configuration file overriding values | {} | +| configModifiers | Configuration object modifier | {} | +| hooks | [Hooks](hooks.md) to apply | {} | +| setup | Setup objects to apply | {} | +| injectOnce | Inject test fields just one for multiple test methods with one test instance | false | +| debug | Enable extension debug output | false | +| reuseApplication | Use the same application instance for multiple tests | false | +| useDefaultExtensions | Use default guicey field extensions | true | +| clientFactory | Custom client factory to use | `DefaultTestClientFactory` | +| managedLifecycle | Managed beans lifecycle simulation | true | + +### Managed lifecycle + +Core application tests (`@TestGuiceyApp`) does not start web part and so lifecycle should not work, +but `Managed` objects often used to initialize core services. + +Guicey core test simulate `Managed` lifecycle (call start and stop methods). +For tests, not requiring lifecycle at all, it might be disabled with: + +```java +@TestGuiceyApp(value = App.class, managedLifecycle = false) +``` + +or + +```java +@RegisterExtension +static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..) + ... + .disableManagedLifecycle() +``` + +!!! note + Application lifecycle will remain: events like `onApplicationStartup` would still be + working (and all registered `LifeCycle` objects would work). Only managed objects ignored. + +### Inject test fields once + +By default, guicey would inject test field values before every test method, even if the same +test instance used (`TestInstance.Lifecycle.PER_CLASS`). This should not be a problem +in the majority of cases because guice injection takes very little time. +Also, it is important for prototype beans, which will be refreshed for each test. + +But it is possible to inject fields just once: + +```java +@TestGuiceyApp(value = App.class, injectOnce = true) +// by default new test instance used for each method, so injectOnce option would be useless +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class PerClassInjectOnceGuiceyTest { + @Inject + Bean bean; + + @Test + public test1() {..} + + @Test + public test2() {..} +} +``` + +In this case, the same test instance used for both methods (`Lifecycle.PER_CLASS`) +and `Bean bean` field would be injected just once (`injectOnce = true`) + +!!! tip + To check the actual fields injection time enable debug (`debug = true`) and + it will [print injection time](debug.md#startup-performance) before each test method: + ``` + [Before each] : 2.05 ms + Guice fields injection : 1.58 ms + ``` + +## Testing web logic + +`@TestDropwizardApp` is useful for complete integration testing (when web part is required): + +```groovy +@TestDropwizardApp(MyApplication.class) +class WebModuleTest { + + @Inject + MyService service + + @Test + public void checkWebBindings(ClientSupport client) { + + Assertions.assertEquals("Sample filter and service called", + client.targetApp("servlet").request().buildGet().invoke().readEntity(String.class)); + + Assertions.assertTrur(service.isCalled()); +``` + +`@TestDropwizardApp` contains the same [annotation options](#annotation-options) as core test, +but without lifecycle simulation (lifecycle managed by started server). + +### Random ports + +In order to start application on random port you can use configuration shortcut: + +```groovy +@TestDropwizardApp(value = MyApplication.class, randomPorts = true) +``` + +!!! note + Random ports setting override exact ports in configuration: + ```groovy + @TestDropwizardApp(value = MyApplication, + config = 'path/to/my/config.yml', + randomPorts = true) + ``` + Also, random ports support both server types (default and simple) + +Real ports could be resolved with [ClientSupport](#client) object. + +### Rest mapping + +Normally, rest mapping configured with `server.rootMapping=/something/*` configuration, but +if you don't use custom configuration class, but still want to re-map rest, shortcut could be used: + +```groovy +@TestDropwizardApp(value = MyApplication.class, restMapping="something") +``` + +In contrast to config declaration, attribute value may not start with '/' and end with '/*' - +it would be appended automatically. + +This option is only intended to simplify cases when custom configuration file is not yet used in tests +(usually early PoC phase). It allows you to map servlet into application root in test (because rest is no +more resides in root). When used with existing configuration file, this parameter will override file definition. + + +## Alternative declaration + +Both extensions could be declared in fields: + +```java +@RegisterExtension +static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(AutoScanApplication.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 2", "bar: 12") + .randomPorts() + .hooks(Hook.class) + .hooks(builder -> builder.disableExtensions(DummyManaged.class)) + .create(); +``` + +The only difference with annotations is that you can declare hooks and setup objects as lambdas directly +(still hooks in static fields will also work). + +```java +@RegisterExtension +static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + ... +``` + +This alternative declaration is intended to be used in cases when guicey extensions need to be aligned with +other 3rd party extensions: in junit you can order extensions declared with annotations (by annotation order) +and extensions declared with `@RegisterExtension` (by declaration order). But there is no way +to order extension registered with `@RegisterExtension` before annotation extension. + +So if you have 3rd party extension which needs to be executed BEFORE guicey extensions, you can use field declaration. + +!!! note + Junit 5 intentionally shuffle `@RegisterExtension` extensions order, but you can always order them with + `@Order` annotation. + +### Start application per test method + +When you declare extensions with annotations or with `@RegisterExtension` in static fields, +application would be started before all test methods and shut down after last test method. + +If you want to start application *for each test method* then declare extension in non-static field: + +```java +@RegisterExtension +TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() + +// injection would be re-newed for each test method +@Inject Bean bean; + +@Test +public void test1() { + Assertions.assertEquals(0, bean.value); + // changing value to show that bean was reset between tests + bean.value = 10 +} + +@Test +public void test2() { + Assertions.assertEquals(0, bean.value); + bean.value = 10 +} +``` + +Also, `@EnableHook` and `@EnableSetup` fields might also be not static (but static fields would also work) in this case: + +```java +@RegisterExtension +TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() + +@EnableSetup +MySetup setup = new MySetup() +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup-object.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup-object.md new file mode 100644 index 000000000..4990341d4 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup-object.md @@ -0,0 +1,449 @@ +# Test environment setup + +It is often required to prepare test environment before starting dropwizard application. +Normally, such cases require writing custom junit extensions. In order to simplify +environment setup, guicey provides `TestEnviromentSetup` interface. + +Setup objects are called before application startup and could directly apply (through parameter) +configuration overrides and hooks. + +!!! info + As [hooks](hooks.md) could modify application configuration, setup object modifies + test extension configuration (hook - extra application functionality, setup object - extra test functionality). + +For example, suppose you need to set up a database before test: + +```java +public class TestDbSetup implements TestEnvironmentSetup { + + @Override + public Object setup(TestExtension extension) throws Exception { + // pseudo code + Db db = DbFactory.startTestDb(); + // register required configuration + extension + .configOverride("database.url", ()-> db.getUrl()) + .configOverride("database.user", ()-> db.getUser()) + .configOverride("database.password", ()-> db.getPassword); + // assuming object implements Closable + return db; + } +} +``` + +It is not required to return anything, only if something needs to be closed after application shutdown: +objects other than `Closable` (`AutoClosable`) or `org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource` +simply ignored. +This approach (only one method) simplifies interface usage with lambdas. + +Setup object might be declared in extension annotation: + +```java +@TestGuiceyApp(value=App.class, setup=TestDbSetup.class) +``` + +Or in manual registration: + +```java +@RegisterExtension +TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + // as class + .setup(TestDbSetup.class) + // or as instance + .setup(new TestDbSetup()) +``` + +Or with lambda: + +```java +.setup(ext -> { + Db db = new Db(); + ext.configOverride("db.url", ()->db.getUrl()) + return db; +}) +``` + +## Setup fields + +Alternatively, setup objects might be declared simply in test fields: + +```java +@EnableSetup +static TestEnvironmentSetup db = ext -> { + Db db = new Db(); + ext.configOverride("db.url", ()->db.getUrl()) + return db; + }; +``` + +or + +```java +@EnableSetup +static TestDbSetup db = new TestDbSetup() +``` + +This could be extremely useful if you need to unify setup logic for multiple tests, +but use different extension declarations in test. In this case simply move field +declaration into base test class: + +```java +public abstract class BaseTest { + + @EnableSetup + static TestDbSetup db = new TestDbSetup(); +} +``` + +!!! note + To avoid confusion with guicey hooks: setup object required to prepare test environment before test (and apply + required configurations) whereas hooks is a general mechanism for application customization (not only in tests). + Setup objects are executed before application startup (before `DropwizardTestSupport` object creation) and hooks + are executed by started application. + +### Custom configuration block + +To simplify field-based declarations, custom (free) block added (`.with()`): + +```java +@RegisterExtension +static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..) + ... + .with(builder -> { + if (...) { + builder.configOverrides("foo.bar", 12); + } + }) +``` + +And the same for setup objects: + +```java +@EnableSetup +static TestEnvironmentSetup setup = ext -> + ... + .with(builder -> { + ... + }) +``` + +## Builder configuration + +`TestExtension` builder provides almost the same options as the main guice extension builder (when declared in field) + +| Method | Description | Example | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| `.config(ThrowingSupplier)` | Manual configuration object creation (config overrides will not work) | `.config(()-> new MyConfig())` | +| `.configOverrides(String...)` | Multiple configuration override values in "key: value" form. | `.configOverrides("foo: 10", "bar: 12")` | +| `.configOverrides(ConfigOverride & ConfigurablePrefix)` | Config override object (used for deferred values) | `.configOverrides(new ConfigOverrideValue("baa", () -> "44"))` | +| `.configOverride(String, String)` | Single config path override | `.configOverride("some.foo", "12")` | +| `.configOverride(String, Supplier)` | Deferred config override value | `.configOverride("foo", () -> "1")` | +| `.configOverrideByExtension(ExtensionContext.Namespace, String)` | 3rd party junit extension integration | `.configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "foo")` | +| `.configOverrideByExtension(ExtensionContext.Namespace, String, String)` | 3rd party junit extension integration | `.configOverrideByExtension(ExtensionContext.Namespace.create("sample"), "storKey", "foo")` | +| `.hooks(GuiceyConfigurationHook)` | Hooks registration | `.hooks(builder -> builder.disableExtensions(Something.class))` | +| `.configModifiers(ConfigModifier...)` | Config modifier registration | `.configModifiers(config -> config.bar = 11)` | +| `.injectOnce()` | Process test fields injection only once (for same test instance) | | +| `.debug()` | Enable [debug](debug.md) output | | +| `.reuseApplication()` | Use [the same application](unification.md#reuse-application-between-tests) instance for multiple tests | | +| `.disableDefaultExtensions()` | Disable setup objects loading with service lookup (and so default extensions) | | +| `.clientFactory(TestClientFactory)` | Custom [web client](client.md) client factory (used in `ClientSupport`) | | + +Specific options: + +| Method | Description | +|-----------------------------------|------------------------------------------------------------------------------| +| `.isDebug()` | Identifies activated debug mode | +| `.isApplicationStartedForClass()` | Shortcut to differentiate application started for test calss or every method | +| `.getJunitContext()` | Access junit `ExtensionContext` | + +### Lifecycle + +Setup object could react on test lifecycle events: `.listen(TestExecutionListener)`: + +```java +public interface TestExecutionListener { + default void starting(final EventContext context) throws Exception {} + default void started(final EventContext context) throws Exception {} + default void beforeAll(final EventContext context) throws Exception {} + default void beforeEach(final EventContext context) throws Exception {} + default void afterEach(final EventContext context) throws Exception {} + default void afterAll(final EventContext context) throws Exception {} + default void stopping(final EventContext context) throws Exception {} + default void stopped(final EventContext context) throws Exception {} +} +``` + + +Complex setup objects might simply `implement TestExecutionListener` and register self: + + +```java +public class MySetup implements TestEnvironmentSetup, TestExecutionListener { + @Override + public Object setup(TestExtension extension) throws Exception { + extension.listen(this); + } + + @Override + public void started(final EventContext context) throws Exception { + // something + } +} +``` + +To simplify usage with setup fields, separate listener methods available to use with lambdas: + +```java +public class Test { + + @EnableSetup + static TestDbSetup db = ext - > ext + .onApplicationStarting(event -> ...) + .onApplicationStart(event -> ...) + .onBeforeAll(event -> ...) + .onBeforeEach(event -> ...) + .onAfterEach(event -> ...) + .onAfterAll(event -> ...) + .onApplicationStopping(event -> ...) + .onApplicationStop(event -> ...) +} +``` + +Events: + +| Listener | Shortcut method | Description | Junit phase | +|--------------|-------------------------|--------------------------------------------------------------------------------------|-------------------------| +| `starting` | `onApplicationStarting` | Just before application starting | BeforeAll or BeforeEach | +| `started` | `onApplicationStart` | Application started | BeforeAll or BeforeEach | +| `beforeAll` | `onBeforeAll` | Before all test methods (**might not be called** if extension registered per method) | BeforeAll or not called | +| `beforeEach` | `onBeforeEach` | Before each test method | BeforeEach | +| `afterEach` | `onAfterEach` | After each test method | AfterEach | +| `afterAll` | `onAfterAll` | After all test methods (**might not be called** if extension registered per method) | AfterAll or not called | +| `stopping` | `onApplicationStopping` | Just before application stopping | AfterAll or AfterEach | +| `stopped` | `onApplicationStop` | Application stopped | AfterAll or AfterEach | + +`EventContext` parameter provides access for guice injector, DropwizardTestSupport object and junit 5 context. + +As you can see, events cover all junit lifecycle events together with application specific +events. Which makes setup objects a complete alternative to pure junit extensions. + +## Auto lookup + +Custom `TestEnvironmentSetup` objects could be loaded automatically +with service loader. New default extensions already use service loader. + +To enable automatic loading of custom extension add: +`META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup` + +And put there required setup object classes (one per line), like this: + +``` +ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordedLogsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.RestStubSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MocksSupport +``` + +Now, when setup objects have more abilities, more custom test extensions could be implemented +(see new filed-based extensions below). Automatic installation for such 3rd party +extensions (using service loader) should simplify overall usage. + +!!! note + Service loading for extensions could be disabled (together with new default extensions): + ```java + @TestGuiceyApp(.., useDefaultExtensions = false) + ``` + +## Annotated fields support + +`TestExtension` builder provides a special method to search annotated test fields: `.findAnnotatedFields()`. + +```java +public class Test { + @MyAnn + Base field; +} +``` + +```java +public class CustomFieldsSupport implements TestEnvironmentSetup { + @Override + public Object setup(TestExtension extension) throws Exception { + + List> fields = extension + .findAnnotatedFields(MyAnn.class, Base.class); + } +``` + +Out of the box, API provides many checks, like required base class (it could be Object to avoid check): +if annotated field type is different - error would be thrown. + +Returned object is also an abstraction: `AnnotatedField` - it simplifies working with filed value, +plus contains additional checks. + +The main idea is keeping annotation, filed and actual value (that must be injected into test field) +in one object (for simplicity - no need to maintain external state). + +### Writing annotated field support + +There is a special **base class** `AnnotatedTestFieldSetup` which implements base fields workflow +(including proper nested tests support). + +Use this class if you want to implement new field annotation (`@MyAnnotation`) support: + +```java +public class MyFieldsSupport extends AnnotatedTestFieldSetup +``` + +If your field value would always base on some class then specify it to automatically +apply related field validations: `AnnotatedTestFieldSetup` + +All current field extensions are using this base class, so you can see usage examples in: + +* `StubFieldsSupport` - [@StubBean](stubs.md) +* `MockFieldsSupport` - [@MockBean](mocks.md) +* `SpyFieldsSupport` - [@SpyBean](spies.md) +* `LogFieldsSupport` - [@RecordLogs](logs.md) +* `TrackerFieldsSupport` - [@TrackBean](tracks.md) +* `RestStubFieldsSupport` - [@StubRest](rest.md) + +Base class would search for all annotated fields and call other methods only if +anything was found. + +!!! important + It is recommended to implement core extension logic inside the hook and use + setup object obly to configure that hook. This way setup object would be simpler. + (all extensions above use separate hooks). + +The following methods should be implemented: + +| Method | Description | Stage | +|----------------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| +| fieldDetected | Validate resolved field, if required. Anything that could not be checked automatically | beforeAll or beforeEach, app not started | +| registerHooks | Register hook instance (hook used to apply extensions, override guice bindings etc.). | beforeAll or beforeEach, app not started | +| initializeField | Here value must be prepared to inject into annotated field. Or user-provided value must be validated | beforeAll or beforeEach, app not started | +| beforeValueInjection | Called just before injecting value into test field. Good point to apply remaining validations (e.g. requireing started injector) | beforeAll and beforeEach (called up to 2 times), app started | +| injectFieldValue | Called to provide field value for injection (if pre-initializerd by user - method not called) | beforeAll and beforeEach (called up to 2 times), app started | +| report | Debug report (list detected fields). Report is called when root extension debug is enabled | beforeAll or beforeEach, app started | +| beforeTest | Called to call lifecycle method before test (like state clearing) | beforeEach, app started | +| afterTest | Called to call lifecycle method after test (like state clearing) | beforeEach, app started | + +Take a look at `MockFieldsSupport` - it is a simple and easy to understand implementation. + +#### fieldDetected + +Method called as soon as field is detected: + +* Ideal place for an additional validations (`TrackerFieldsSupport` validates field type there) +* This is the earliest point: `LogFieldsSupport` use it to activate logger immediately + +#### registerHooks + +Usually simple hook registration. Only `RestStubFieldsSupport` use it to register 2 hooks +(second hook validates application scope: in theory could be implemented in one hook but guicey implements generic +hooks which could be used without junit). + +#### initializeField + +Here we validate user-provided value or create new value. + +For example, mocks hook (`MockFieldsSupport`): + +```java +@Override +@SuppressWarnings("unchecked") +protected void initializeField(final AnnotatedField field, final Object userValue) { + final Class type = field.getType(); + if (userValue != null) { + Preconditions.checkState(MockUtil.isMock(userValue), getDeclarationErrorPrefix(field) + + "initialized instance is not a mockito mock object. Either provide correct mock or remove value " + + "and let extension create mock automatically."); + hook.mock(type, (K) userValue); + } else { + // no need to store custom data for manual value - injectFieldValue not called for manual values + field.setCustomData(FIELD_MOCK, hook.mock(type)); + } +} +``` + +Note that value is stored inside an `AnnotatedField` object: `field.setCustomData(FIELD_MOCK, hook.mock(type));` +(for user-provided value, it is stored automatically). + +This is a not required step: for example, `LogFieldsSupport` create value object just after field detection +(because logger must be appended as soon as possible), and so ignored `initializeField` method. + +Another example is `StubFieldsSupport` - where `initializeField` method used just for +stub registration in hook. Value for injection into test field is obtained later directly +from guice injector (stub could be declared by class - instance is guice managed). + +#### beforeValueInjection + +For remaining validation (when injector is required). For example, `SpyFieldsSupport` +use it to validatate if target bean is managed by guice (spy use AOP and can't work with bean bound by instance) +Same story for `TrackerFieldsSupport`. + +There is even a helper method to validate non-instance bindings: `isInstanceBinding(binding)` + +#### injectFieldValue + +Method called **only for not pre-initialized** fields (no user value). + +In most cases, it just provides a value, created in `initializeField`: + +```java +@Override +protected Object injectFieldValue(final EventContext context, final AnnotatedField field) { + return Preconditions.checkNotNull(field.getCustomData(FIELD_MOCK), "Mock not created"); +} +``` + +Stubs extension rely on guice context (because stub could be guice-meneged): + +```java +@Override +protected Object injectFieldValue(final EventContext context, final AnnotatedField field) { + // if not declared, stub value created by guice + return context.getBean(field.getAnnotation().value()); +} +``` + +#### report + +Report assumed to show detected fields when root extension debug is enabled. See example report in any extension. + +#### beforeTest and afterTest + +Special methods for implementing field value lifecycle. +Almost all values have to be reset after each test method (mocks, spies, stubs etc.). + +Example from logs extension: + +```java + @Override +protected void afterTest(final EventContext context, + final AnnotatedField field, final RecordedLogs value) { + if (field.getAnnotation().autoReset()) { + value.clear(); + } +} +``` + +Mocks and speies use this method also to print summary report (if requested in annoation): + +```java +@Override +@SuppressWarnings("PMD.SystemPrintln") +protected void afterTest(final EventContext context, + final AnnotatedField field, final Object value) { + if (field.getAnnotation().printSummary()) { + final String res = Mockito.mockingDetails(value).printInvocations(); + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + + "@" + MockBean.class.getSimpleName() + " stats on [After each] for " + + TestSetupUtils.getContextTestName(context.getJunitContext()) + ":\n\n" + + Arrays.stream(res.split("\n")).map(s -> "\t" + s).collect(Collectors.joining("\n"))); + } + if (field.getAnnotation().autoReset()) { + Mockito.reset(value); + } +} +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup.md new file mode 100644 index 000000000..975311a99 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/setup.md @@ -0,0 +1,80 @@ +# JUnit 5 + +!!! note "" + Junit 5 [user guide](https://junit.org/junit5/docs/current/user-guide/) | [Migration from JUnit 4](../junit4.md#migrating-to-junit-5) + +## Setup + +You will need the following dependencies (assuming BOM used for versions management): + +```groovy +testImplementation 'io.dropwizard:dropwizard-testing' +testImplementation 'org.junit.jupiter:junit-jupiter-api' +testRuntimeOnly 'org.junit.jupiter:junit-jupiter' +``` + +!!! tip + If you already have junit4 or spock tests, you can activate [vintage engine](https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4) + so all tests could work **together** with junit 5: + ```groovy + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + ``` + +!!! note + In gradle you need to explicitly [activate junit 5 support](https://docs.gradle.org/current/userguide/java_testing.html#using_junit5) with + ```groovy + test { + useJUnitPlatform() + ... + } + ``` + +!!! warning + Junit 5 annotations are **different** from junit4, so if you have both junit 5 and junit 4 + make sure correct classes (annotations) used for junit 5 tests: + ```java + import org.junit.jupiter.api.Assertions; + import org.junit.jupiter.api.Test; + ``` + +## Dropwizard extensions compatibility + +Guicey extensions *could be used together with dropwizard +extensions*. It could be used to start multiple dropwizard applications. + +For example: + +```java +// run app (injector only) +@TestGuiceyApp(App.class) +// activate dropwizard extensions +@ExtendWith(DropwizardExtensionsSupport.class) +public class ClientSupportGuiceyTest { + + // Use dropwizard extension to start a separate server + // It might be the same application or different + // (application instances would be different in any case) + static DropwizardAppExtension app = new DropwizardAppExtension(App.class); + + @Test + void testLimitedClient(ClientSupport client) { + Assertions.assertEquals(200, client.target("http://localhost:8080/dummy/") + .request().buildGet().invoke().getStatus()); + } +} +``` + +!!! info + There is a difference in extensions implementation. + + Dropwizard extensions work as: + junit extension `@ExtendWith(DropwizardExtensionsSupport.class)` looks for fields + implementing `DropwizardExtension` (like `DropwizardAppExtension`) and start/stop them according to test lifecycle. + + Guicey extensions implemented as separate junit extensions (only some annotated fields are manually searched + (hooks, setup objects, special extensions). + Also, guciey extensions implement junit parameters injection (for test and lifecycle methods). + + + + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/spies.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/spies.md new file mode 100644 index 000000000..6c17da62f --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/spies.md @@ -0,0 +1,242 @@ +# Testing with spies + +[Mockito](https://site.mockito.org/) spies allows dynamic modification of real objects behavior +(configured same as [mocks](mocks.md), but, by default, all methods work as in raw bean). + +Spies declared with a `@SpyBean` annotation. + +!!! important + Spy creation requires real bean instance and so guicey use AOP to intercept real bean + access and redirecting all calls through a dynamically created (on first access) + spy object. This means that spies would only work with guice-managed beans. + + If you need to spy for a manual instance - use [partial mocks](mocks.md#partial-mocks) + +!!! warning + Spies will not work for HK2 beans + +Mockito documentation is written in the `Mockito` class [javadoc](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html). +Additional docs could be found in [mockito wiki](https://github.com/mockito/mockito/wiki/FAQ) +Also, see official [mockito refcard](https://dzone.com/refcardz/mockito) +and [baeldung guides](https://www.baeldung.com/mockito-series). + +## Setup + +Requires mockito dependency (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +Suppose we have a service: + +```java +public static class Service { + + public String get(int id) { + return "Hello " + id; + } +} +``` + +Spying it: + +```java +@TestGuiceyApp(App.class) +public class Test { + + @SpyBean + Service spy; + + // NOT the same instance as spy (but calls on both objects are equivalent) + @Inject + Service service; + + @BeforeEach + public void setUp() { + // IMPORTANT: spies configured in reverse order to avoid accidental method call + doReturn("bar1").when(spy).get(11); + } + + @Test + public void test() { + // stubbed result + Assertions.assertEquals("bar1", s1.get(11)); + // real method result (because argument is different) + Assertions.assertEquals("Hello 10", s1.get(10)); + } +} +``` + +Here `doReturn` refer to `Mockito.doReturn` used with static import. + +!!! note + As real guice bean used under the hood, all AOP, applied to the original bean, will work. + +!!! tip + Calling guice proxy `service.get(11)` and spy object + directly `spy.get(11)` is equivalent (because guice returns AOP proxy which redirects + call to the spy) + +See other examples in [mocks section](mocks.md#mocking-examples). + +## Asserting calls + +!!! tip + If you want to use spies to track bean access (verify arguments and response) then + try [trackers](tracks.md) which are better match for this case. + +As [mocks](mocks.md#asserting-calls), spies could be used to assert calls: + +```java +// method Service.get(11) called on mock just once +verify(spy, times(1)).get(11); +``` + +These assertions would fail if method was called more times or using different arguments. + +## Method result capture + +Verifying method return value with spies is a bit clumsy: + +```java +public static class ResultCaptor implements Answer { + private T result = null; + public T getResult() { + return result; + } + + @Override + public T answer(InvocationOnMock invocationOnMock) throws Throwable { + result = (T) invocationOnMock.callRealMethod(); + return result; + } +} + +@TestGuiceyApp(App.class) +public class Test { + ResultCaptor resultCaptor = new ResultCaptor<>(); + // capture actual argument value (just to show how to do it) + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + + @SpyBean + Service spy; + + @BeforeAll + public void setUp() { + doAnswer(resultCaptor).when(spy).get(argumentCaptor.capture()); + } + + public void test() { + // call method + Assertions.assertThat(spy.get(11)).isEqualTo("bar"); + // result captured + Assertions.assertThat(resultCaptor.getResult()).isEqualTo("bar"); + Assertions.assertThat(argumentCaptor.getValue()).isEqualTo(11); + + verify(spy, Mockito.times(1)).get(11); + } +} +``` + +Why would you need that? It is often useful when verifying indirect bean call. +For example, if we have `SuperService` which internally calls `Service` and so +there is no other way to verify service call result correctness other than spying it (or use [tracker](tracks.md)). + +## Pre initialization + +As spy object creation is delayed until application startup, it is impossible to +configure spy before application startup (as with mocks). Usually it is not a problem, +if target bean is not called during startup. + +If you need to modify behavior of spy, used during application startup (e.g. by some `Managed`), +then there is a delayed initialization mechanism: + +```java +// extra class required to overcome annotation limitation +public class Initializer implements Consumer { + // real spy could be created ONLY after injector startup + @Override + public void accept(Service spy) { + doReturn("spied").when(service).get(11); + } + +} + +@TestGuiceyApp(App.class) +public class Test { + + @SpyBean(initializers = Initializer.class) + Service spy; + + ... +} +``` + +Here, `Initializer` would be called just after spy creation (on first access). + +And so any `Managed`, calling it during startup would use completely configured spy: + +```java +@Singleton +public static class Mng implements Managed { + @Inject + Service service; + + @Override + public void start() throws Exception { + // "spied" result + service1.get(11); + } +} +``` + +## Spies reset + +Spies are re-set automatically after each test method (and that's why it makes +sense to declare mock behavior in test setup method - execured before each test method). + +!!! note + Spy could be reset manually at any time with `Mockito.reset(spy)` + +Spies automatic reset could be disabled with `autoReset` option: + +```java +@SpyBean(autoReset = false) +Service spy; +``` + +## Spies report + +Same as for mocks, a usage report could be printed after each test `@SpyBean(printSummary = true)` + +``` +\\\------------------------------------------------------------/ test instance = 285bf5ac / +@SpyBean stats on [After each] for SpySummaryTest$Test1#test(): + + [Mockito] Interactions of: ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Service$$EnhancerByGuice$$60e90c@40fe8fd5 + 1. spySummaryTest$Service$$EnhancerByGuice$$60e90c.foo( + 1 + ); + -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Test1.test(SpySummaryTest.java:50) +``` + +## Debug + +When extension debug is active: + +```java +@TestGucieyApp(value = App.class, debug = true) +public class Test +``` + +All recognized spy fields would be logged: + +``` +Applied spies (@SpyBean) on SpySimpleTest: + + #spy2 Service2 (r.v.d.g.t.j.s.s.SpySimpleTest) + #spy1 Service1 (r.v.d.g.t.j.s.s.SpySimpleTest) +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/startup.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/startup.md new file mode 100644 index 000000000..51a161b0c --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/startup.md @@ -0,0 +1,43 @@ +# Testing startup error + +!!! warning + Commands execution overrides System IO and so can't run in parallel with other tests! + + Use [`@Isolated`](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution-synchronization) + on such tests to prevent parallel execution with other tests + +Tests for application startup fail often required to check some startup conditions. +The problem is that it's not enough to simply run the application with "bad" configuration file +because on error application calls `System.exit(1)`: + +```java + public abstract class Application { + ... + protected void onFatalError(Throwable t) { + System.exit(1); + } +} +``` + +Instead, you can use command run utility: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .runApp() +``` + +or with the shortcut: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .runApp() +``` + +!!! tip + [Test framework-agnostic utilities](../general/general.md) provides simple utilities to run application + (core or web). Could be useful when testing several applications interaction. + +!!! important + In case of application *successful* start, special check would immediately stop it + by throwing exception (resulting object would contain it), so such test would never freeze. + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/stubs.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/stubs.md new file mode 100644 index 000000000..946b9c6ba --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/stubs.md @@ -0,0 +1,127 @@ +# Testing with stubs + +Stubs are hand-made replacements of real application services ("manual" or "lazy" [mocks](mocks.md)). + +Stubs declared in test class with a new `@StubBean` annotation. + +!!! warning + Stubs will not work for HK2 beans + +There are two main cases: + +1. Stub class extends existing service: `class ServiceStub extends Service` +2. Stub implements service interface: `class ServiceStub implements IService` + +Stubs replace real application services (using guice [overriding modules](../overview.md#guice-bindings-override)), +so stub would be injected in all services instead of the real service. + +For example, suppose we have a service: + +```java +public class Service { + public String foo() { + ... + } +} +``` + +where method foo implements some complex logic, not required in test. + +Writing stub: + +```java +public class ServiceStub extends Service { + @Override + public String foo() { + return "static value"; + } +} +``` + +Using stub in test: + +```java +@TestGuiceyApp(App.class) +public class Test { + + @StubBean(Service.class) + ServiceStub stub; + + // injecting here to show that stub replaced real service + @Inject + Service service; + + @Test + public void test(){ + // service is a stub + Assertions.assertInstanceOf(ServiceStub.class, service); + Assertions.assertEquals("static value", service.foo()); + } +} +``` + +!!! info + In many cases, mockito [mocks](mocks.md) and [spies](spies.md) could be more useful, + but stubs are simpler (easier to understand, especially comparing to spies). + +In the example above, stub instance is created by guice. +Stub could also be registered by instance: + +```java +@StubBean(Service.class) +ServiceStub stub = new ServiceStub(); +``` + +In this case, stub's `@Inject` fields would be processed (`requestInjection(stub)` would be called). + +!!! note + When stub is registered with instance, stub field must be static for per-test application run + (default annotation). It may not be static for per-method application startup (with `@RegisterExtension`). + +!!! note + Guice AOP would apply only for stubs registered with class. So stub instance + could be used (instead of class) exactly to avoid additional AOP logic for service. + +## Stub lifecycle + +More complex stubs may contain a test-related internal state, which must be cleared between tests. + +In this case, stub could implement `StubLifecycle`: + +```java +public class ServiceStub extends Service implements StubLifecycle { + int calls; + + @Override + public void before() { + calls = 0; + } + + @Override + public void after() { + calls = 0; + } +} +``` + +(both methods optional) + +Such methods would be called automatically before and after of each test method. + +## Debug + +When extension debug is active: + +```java +@TestGucieyApp(value = App.class, debug = true) +public class Test +``` + +All recognized stub fields would be logged: + +``` +Applied stubs (@StubBean) on StubsSimpleTest: + + StubsSimpleTest.stub2 GUICE Service2 >> Service2Stub + StubsSimpleTest.stub GUICE Service1 >> Service1Stub +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/test-ext.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/test-ext.md new file mode 100644 index 000000000..46d681c01 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/test-ext.md @@ -0,0 +1,107 @@ +# Testing extensions + +Extensions (like [setup objects](setup-object.md)) often rely on afterEach/afterAll methods +and so it is not possible to test extension completely using test extensions (like `@TestGuiceyApp`). + +Junit provides a TestKit which could run unit tests inside usual unit test. This way +full extension lifecycle could be tested. + +Additional dependency required (version managed by dropwizard BOM): + +```groovy +testImplementation 'org.junit.platform:junit-platform-testkit' +``` + +Prepare test class, using your extension (better inner class): + +```java +public class Test { + + + // IMPORTANT to skip this test for the main junit engine (don't let it run this test) + @Disabled + public static class TestCase1 { + + // custom extension + @MyAnnotation + Something field; + + } +} +``` + +Running test: + +```java +Throwable th; +EngineTestKit + .engine("junit-jupiter") + // ignore @Disable annotation + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(DiscoverySelectors.selectClass(TestCase1.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + th = err; + }); + +if (th != null) { + // success case +} else { + // error case +} +``` + +## Testing console output + +Full console output could be tracked wither with [output captor utility](output.md) or +using `system-stubs-jupiter` library. + +For example: + +```java +public void run(Class test) { + EngineTestKit + .engine("junit-jupiter") + .... +} + +@Test +public void test() { + String out = TestSupport.captureOutput(() -> { + run(TestCase1.class); + }); + + // windows compatibility + out = out.replace("\r",""); + + Assertions.assertThat(out).contains("some probably long text"); +} +``` + +Most likely, logs would contain some changing data (like logger time or performance measures), +so output would need to be pre-processed with regexps. + +For example, to replace string like "20 ms", "112.3 ms": + +```java +out.replaceAll("\\d+(\\.\\d+)? ms( +)?", "111 ms "); +``` + +To replace lambda identity in class name: + +```java +out.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") + // jdk 21 + .replaceAll("\\$\\$Lambda/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111"); +``` + +Logger time: + +```java +out.replaceAll("\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d+]", "[2025-22-22 11:11:11]") +``` + +And so on. You can see `AbstractPlatformTest` in guicey tests (dropwizard-guicey module) and +all related tests as examples. \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/tracks.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/tracks.md new file mode 100644 index 000000000..f10c1b21b --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/tracks.md @@ -0,0 +1,360 @@ +# Testing performance (bean tracking) + +Tracker records guice bean methods execution: + +1. Collect method call arguments and result for each call +2. Log slow methods execution +3. Collect metrics to show overall methods performance (stats) + +Tracker is declared with `@TrackBean` annotation. + +!!! warning + Trackers will not work for HK2 beans and for non guice-managed beans (bound by instance) + +!!! note + Initially, trackers were added as a simpler alternative for [mockito spy's + clumsy result capturing](spies.md#method-result-capture). But, eventually, it evolved into a simple performance tracking + tool (very raw, of course, but in many cases it would be enough). + +## Setup + +Not strictly required, but trackers provide type-safe search api using mockito, and so +you'll need mockito dependency **only if** you wish to use this api (version may be omitted if dropwizard BOM used): + +```groovy +testImplementation 'org.mockito:mockito-core' +``` + +## Usage + +Suppose we have a service: + +```java +public static class Service { + + public String get(int id) { + return "Hello " + id; + } +} +``` + +And we want to very indirect service call (when service called by some other service): + +```java +@TestGuiceyApp(App.class) +public class Test { + @TrackBean(trace = true) + Tracker tracker; + + @Inject + Service service; + + @Test + public void test() { + // call service + Assertions.assertEquals("Hello 11",service.get(11)); + + MethodTrack track = tracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("get(11) = \"Hello 11\"")); + // object arguments + Assertions.assertArrayEquals(new Object[] {11},track.getRawArguments()); + // arguments in string form + Assertions.assertArrayEquals(new String[] {"11"},track.getArguments()); + // raw result + Assertions.assertEquals("1 call",track.getRawResult()); + // result in string form + Assertions.assertEquals("1 call",track.getResult()); + } +} +``` + +In this example, trace was enabled (optional) and so each method call would be logged like this: + +``` +\\\---[Tracker] 0.41 ms <@1b0e9707> .get(11) = "Hello 11" +``` + +## Configuration + +`@TrackBean` annotation options: + +| Option | Description | Default | +|-----------------|-----------------------------------------------------------------------------------------------------------|-----------| +| trace | When enabled, all method calls are printed | false | +| slowMethods | Print warnings about methods executing longer than the specified threshold. Set to 0 to disable warnings. | 5 seconds | +| slowMethodsUnit | Unit for slowMethods value | Seconds | +| keepRawObjects | Keep method call arguments and result objects (potentially mutable) | true | +| maxStringLength | Max length for a `String` argument or result (cut long strings) | 30 | +| autoReset | Clear trackers after each test method | true | +| printSummary | Print summary for exact tracker after each test method | false | + +### Tracing + +Tracing might be useful to see each tracked method call in console with parameters and execution time: + +``` +\\\---[Tracker] 0.41 ms <@1b0e9707> .foo(1) = "1 call" +\\\---[Tracker] 0.02 ms <@1b0e9707> .foo(2) = "2 call" +\\\---[Tracker] 0.12 ms <@1b0e9707> .bar(1) = "1 bar" +``` + +It also prints service instance hash, to make obvious method calls on different instances. +Different instances could appear on prototype-scoped beans (default scope). + +Enabled with: + +```java +@TrackBean(trace = true) +``` + +!!! note + Traces are logged with `System.out` to make sure messages are always visible in console. + +### Slow methods + +By default, tracker would log methods, executed longer than 5 seconds: + +``` +WARN [2025-05-09 08:30:38,458] ru.vyarus.dropwizard.guice.test.track.Tracker: +\\\---[Tracker] 7.07 ms <@7634f2b> .foo() = "foo" +``` + +!!! note + Slow methods are logged with **logger**, and not `System.out` as traces. + +For example, to set slow method for 1 minute: + +```java +@TrackBean(slowMethod = 1, slowMethodsUnit = ChronoUnit.MINUTES) +``` + +To avoid logging slow methods: + +```java +@TrackBean(slowMethod = 0) +``` + +### Keeping raw objects + +By default, tracker stores all arguments and returned result objects. + +Raw arguments could be used to examine complex objects just after the method call. +But, in case of multiple method calls, raw objects might not be actual. For example: + +```java +public Service { + public void foo(List list) { + list.add("foo" + list.size()); + } +} +``` + +Here method changes argument state and so, if we call method multiple times, stored arguments +would be useless (as all calls would reference the same list instance): + +```java +List test = new ArrayList<>(); +service.foo(test); +service.foo(test); + +// stored list useless as object was changed after the initial call +List firstCallArg = tracker.getLastTracks(2).get(0).getRawArguments().get(0); +Assertions.assertEquals(2, firstCallArg.size()); + +// but string representation would still be useful: +String firstCallArgString = tracker.getLastTracks(2).get(0).getArguments().get(0); +Assertions.assertEquals("0[]", firstCallArg.size()); + +// second call argument string +String firstCallArgString = tracker.getLastTracks(2).get(1).getArguments().get(0); +Assertions.assertEquals("1['foo1']", firstCallArg.size()); +``` + +In case of complex objects (pojo, for example), string representation would only contain +the type and instance hash: `Type@hash` (which is not informative, but the only universal short +way to describe object). + +If tracker used only for performance testing (to accumulate execution time from many runs), +it might make sense to avoid holding raw arguments: + +```java +@TrackBean(keepRawObjects = false) +``` + +### Max length + +Methods could consume or return large string, but using large stings for console +output is not desired. All strings larger then configured size would be cut with "..." suffix: + +``` +\\\---[Tracker] 0.08 ms <@66fb45e5> .baz("largelargelargelargelargelarge...") +``` + +Changing default: + +```java +@TrackBean(maxStringLength = 10) +``` + +### Print summary + +By default, extension print overall stats report when [extension debug enabled](#debug). + +For each tracker, an individual report could be activated with: `@TrackBean(printSummary = true)` +This report does not depend on the extension debug flag. + +The summary report also shows the number of service instances involved in stats (in the example +trace was enabled for clarity): + +``` +\\\---[Tracker] 0.28 ms <@6707a4bf> .foo(1) = "foo1" +\\\---[Tracker] 0.007 ms <@79d3473e> .foo(2) = "foo2" + +\\\------------------------------------------------------------/ test instance = 51f18e31 / +Tracker stats (sorted by median) for ReportForMultipleInstancesTest$Test1#testTracker(): + + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + Service foo(int) 2 (2) 0 0.007 ms 0.281 ms 0.281 ms 0.281 ms 0.281 ms +``` + +Note different instances in trace (`<@6707a4bf>`, `<@79d3473e>`) and instances count in calls column: `2 (2)` + + +## Tracked data + +Each call stored as `MethodTrack` and contains raw arguments `getRawArguments()` (which might change over time +if mutable objects used) and string version `getArguments()` (can't change) and same for the result object. +Raw objects are mostly useful in case of immediate check after the method call. + +Same for result: `getRawResult()` for raw object and `getResult()` for string version. + +Also, there are quoted string versions: `getQuatedResult()` and `getQuatedArguments()`. +These methods are the same as string methods, but all strings are in quotes to clearly see +string bounds (quoted versions useful for console reporting) + +Obtaining tracked data: + +```java +// all recordings +List tracks = tracker.getTracks(); +// last 2 calls (in execution order) +List tracks = tracker.getLastTracks(2); +// last call +MethodTrack track = tracker.getLastTrack(); +``` + +### Searching + +In the case of many recorded executions (for multiple methods), search could be used: + +```java +// search by method (any argument value) +tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.anyInt())) + ); + +// search methods with argument condition ( > 1) +tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument > 1))) + ); + +// search for methods with exact argument value +tracks = tracker.findTracks(mock -> when( + mock.foo(11)) + ); +``` + +This method uses Mockito stubbing abilities for search criteria declaration: +easy to use and type-safe search. + +### Reset data + +Tracked data could be cleared at any time either on tracker: `tracker.clear()`. +By default, data is cleared after each test method. + +To disable automatic cleanup: `@TrackBean(autoReset = false)` + +## Debug + +When extension debug is active: + +```java +@TestGucieyApp(value = App.class, debug = true) +public class Test +``` + +All recognized tracker fields would be logged: + +``` +Applied trackers (@TrackBean) on TrackerSimpleTest: + + #serviceTracker Service (r.v.d.g.t.j.s.t.TrackerSimpleTest) +``` + +Also, a performance report for **all registered tracker objects** would be printed: + +``` +\\\------------------------------------------------------------/ test instance = 2bbb44da / +Trackers stats (sorted by median) for TrackerSimpleTest#testTracker(): + + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + Service foo(int) 3 0 0.011 ms 0.161 ms 0.151 ms 0.161 ms 0.161 ms + Service bar(int) 1 0 0.066 ms 0.066 ms 0.066 ms 0.066 ms 0.066 ms +``` + +## Stats + +Tracker could aggregate all executions of the same method: + +```java +TrackerStats stats = tracker.getStats(); +Assertions.assertEquals(1, stats.getMethods().size()); + +MethodSummary summary = stats.getMethods().get(0); +Assertions.assertEquals("foo", summary.getMethod().getName()); +Assertions.assertEquals(Service.class, summary.getService()); +Assertions.assertEquals(1, summary.getTracks()); +Assertions.assertEquals(0, summary.getErrors()); +Assertions.assertEquals(1, summary.getMetrics().getValues().length); +Assertions.assertTrue(summary.getMetrics().getMin() < 1000); +``` + +Tracker use dropwizard metrics, so stats provide common values like mean time, median time, 95 percentile, etc. + +There is a default statistics report implementation, which might be used for console reporting: + +```java +System.out.println(tracker.getStats().render()); +``` + +```java + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + Service foo(int) 2 (2) 0 0.009 ms 0.352 ms 0.352 ms 0.352 ms 0.352 ms +``` + +Here you can see that 2 instances were used for 2 success calls. Of course max time +would be too large (cold jvm), but with min value you can see more realistic time. +With a high number of executions percentile and mean values would become more realistic. + +Here is an example of tracking `GuiceyConfigurationInfo` with activated `.printAllGuiceBindings()` report: + +``` + [service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] + GuiceyConfigurationInfo getNormalModuleIds() 1 0 1.076 ms 1.076 ms 1.076 ms 1.076 ms 1.076 ms + GuiceyConfigurationInfo getModulesDisabled() 1 0 0.038 ms 0.038 ms 0.038 ms 0.038 ms 0.038 ms + GuiceyConfigurationInfo getOverridingModuleIds() 1 0 0.034 ms 0.034 ms 0.034 ms 0.034 ms 0.034 ms + GuiceyConfigurationInfo getExtensionsDisabled() 1 0 0.020 ms 0.020 ms 0.020 ms 0.020 ms 0.020 ms + GuiceyConfigurationInfo getOptions() 1 0 0.005 ms 0.005 ms 0.005 ms 0.005 ms 0.005 ms + GuiceyConfigurationInfo getData() 3 0 0.003 ms 0.006 ms 0.004 ms 0.006 ms 0.006 ms + +``` + +!!! note + Methods sorted by slowness + +You can also collect stats for multiple trackers: + +```java +TrackerStats overall = new TrackerStats(tracker1, tracker2); +System.out.println(overall.render()); +``` diff --git a/dropwizard-guicey/src/doc/docs/guide/test/junit5/unification.md b/dropwizard-guicey/src/doc/docs/guide/test/junit5/unification.md new file mode 100644 index 000000000..5dd277b2c --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/junit5/unification.md @@ -0,0 +1,109 @@ +# Extension configuration unification + +It is a common need to run multiple tests with the same test application configuration +(same config overrides, same hooks etc.). +Do not configure it in each test, instead move extension configuration into base test class: + +```java +@TestGuiceyApp(...) +public abstract class AbstractTest { + // here might be helper methods +} +``` + +And now all test classes should simply extend it: + +```java +public class Test1 extends AbstractTest { + + @Inject + MyService service; + + @Test + public void testSomething() { ... } +} +``` + +If you use manual extension configuration (through field), just replace annotation in base class with +manual declaration - approach would still work. + +## Meta annotation + +You can prepare meta annotation (possibly combining multiple 3rd party extensions): + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@TestDropwizardApp(AutoScanApplication.class) +public @interface MyApp { +} + +@MyApp +public class MetaAnnotationDwTest { + + @Test + void checkAnnotationRecognized(Application app) { + Assertions.assertNotNull(app); + } +} +``` + +OR you can simply use base test class and configure annotation there: + +```java +@TestDropwizardApp(AutoScanApplication.class) +public class BaseTest {} + +public class ActualTest extends BaseTest {} +``` + +## Reuse application between tests + +In some cases it is preferable to start application just once and use for all tests +(e.g. due to long startup or time-consuming environment preparation). + +In order to use the same application instance, extension declaration must be performed in +[base test class](#extension-configuration-unification) and `reuseApplication` flag must be enabled: + +```java +@TestGuiceyApp(value = Application.class, reuseApplication = true) +public abstract class BaseTest {} +``` + +or + +```java +public abstract class BaseTest { + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .reuseApplication() + .create(); + +} +``` + +The same will work for dropwizard extension (`@TestDropwizardApp` and `TestDropwizardAppExtension`). + +!!! important + Application instance re-use is not enabled by default for backwards compatibility + (for cases when base class declaration already used). + +There might be multiple base test classes declaring reusable applications: +different global applications would be started for each declaration (allowing you +to group tests requiring different applications) + +Global application would be closed after all tests execution (with test engine shutdown). + +In essence, reusable application "stick" to declaration in base class, so all tests, +extending base class "inherit" the same declaration and so the same application (when reuse enabled). + +!!! tip + Reusable applications may be used together with tests, not extending base class + and using guicey extensions. Such tests would simply start a new application instance. + Just be sure to avoid port clashes when using reusable dropwizard apps (by using `randomPorts` option). + +`@EnableSetup` and `@EnableHook` fields are also supported for reusable applications. +But declare all such fields on base class level (or below) because otherwise only fields +declared on first started test would be used. Warning would be printed if such fields used +(or ignored because reusable app was already started by different test). + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/openapi-server.md b/dropwizard-guicey/src/doc/docs/guide/test/openapi-server.md new file mode 100644 index 000000000..7ebf4370a --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/openapi-server.md @@ -0,0 +1,230 @@ +# Openapi fake server + +!!! note "" + See [example source](https://github.com/xvik/dropwizard-guicey/tree/master/examples/openapi-client-server) + +If you generate external API client using OpenAPI declaration, +you can also generate a fake server implementation from the same file. + +Sample client and server build: + +```groovy +mport org.openapitools.generator.gradle.plugin.tasks.GenerateTask + +plugins { + id 'org.openapi.generator' +} + +java { + withSourcesJar() +} + +dependencies { + implementation 'io.dropwizard:dropwizard-forms' + implementation 'com.github.scribejava:scribejava-core:8.3.3' + + // OPENAPI CODEGEN + // additional dependency required for codegen, but conflicts with dropwizard (need to disable feature) + implementation 'org.glassfish.jersey.media:jersey-media-json-jackson' + + compileOnly 'io.swagger.core.v3:swagger-annotations:2.2.30' +} + +// https://openapi-generator.tech/docs/generators/java +openApiGenerate { + generatorName = "java" + inputSpec = "$projectDir/src/main/openapi/petStore.yaml" + outputDir = "$buildDir/petstore/client" + apiPackage = "com.petstore.api" + invokerPackage = "com.petstore" + modelPackage = "com.petstore.api.model" + configOptions = [ + library: "jersey3", + dateLibrary: "java8", + openApiNullable: "false", + hideGenerationTimestamp: "true" + ] +} + + +// https://openapi-generator.tech/docs/generators/jaxrs-jersey +tasks.register('openApiGenerateServer', GenerateTask) { + group = 'openapi tools' + generatorName = "jaxrs-jersey" + inputSpec = "$projectDir/src/main/openapi/petStore.yaml" + outputDir = "$buildDir/petstore/server" + apiPackage = "com.petstore.server.api" + invokerPackage = "com.petstore.server" + modelPackage = "com.petstore.server.api.model" + configOptions = [ + library : "jersey3", + dateLibrary: "java8", + openApiNullable: "false", + hideGenerationTimestamp: "true" + ] +} + +compileJava.dependsOn 'openApiGenerate', 'openApiGenerateServer' +tasks.sourcesJar.dependsOn 'openApiGenerate', 'openApiGenerateServer' +sourceSets.main.java.srcDir "${openApiGenerate.outputDir.get()}/src/main/java" +// note main folder not attached! (sources were copied manually) +sourceSets.main.java.srcDir "${openApiGenerateServer.outputDir.get()}/src/gen/java" +``` + +`openApiGenerate` creates client in build/petstore/client +`openApiGenerateServer` creates server stub in build/petstore/server + +## Client + +Actual client interfaces are generated in: + +``` +/build/petstore/client/src/main/java/com/petstore/api +``` + +These are the main client classes: + +* `PetApi` +* `StoreApi` +* `UserApi` + +Guice bindings: + +```java +public class PetStoreApiModule extends DropwizardAwareModule { + + @Override + protected void configure() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(configuration().getPetStoreUrl()); + // optional + apiClient.setDebugging(true); + + bind(ApiClient.class).toInstance(apiClient); + bind(PetApi.class).toInstance(new PetApi(apiClient)); + bind(StoreApi.class).toInstance(new StoreApi(apiClient)); + bind(UserApi.class).toInstance(new UserApi(apiClient)); + } +} +``` + +(target url is in configuration) + + +## Server + +Generated server stub: + +``` +/build/petstore/server/src/main/java/com/petstore/server +``` + +NOTE: server will also generate its own model, but this part will be attached directly (from `gen` folder). + +Now copy server files into the main sources (preserving package): + +``` +/build/petstore/server/src/main/java/com/petstore/server --> /src/main/java/com/petstore/server +``` + +(except `Bootstrap` class) + +Implement fakes in `impl`. For example, to implement getPetById, change `PetApiServiceImpl` : + +```java +@Override +public Response getPetById(Long petId, SecurityContext securityContext) throws NotFoundException { + final Pet pet = new Pet(); + pet.setId(petId); + pet.setName("Jack"); + final Tag tag = new Tag(); + tag.setName("puppy"); + pet.getTags().add(tag); + return Response.ok().entity(pet).build(); +} +``` + +Now implement root fake resource: + +```java +@Path("/fake/petstore/") +public class FakePetStoreServer { + + // IMPORTANT: paths in console would contain /pet/pet duplicate, but ACTUAL path matching would IGNORE + // @Path("/pet") declared on ApiApi class, so such declaration is correct for runtime + + @Path("/pet") + public Class getPetApi() { + return PetApi.class; + } + + @Path("/store") + public Class getStoreApi() { + return StoreApi.class; + } + + @Path("/user") + public Class getUserApi() { + return UserApi.class; + } +} +``` + +## Bundle + +Use GuiceyBundle for activation: + +```java +public class PetStoreBundle implements GuiceyBundle { + + @Override + public void run(GuiceyEnvironment environment) throws Exception { + // because of required conflicting dependency jersey-media-json-jackson + // https://github.com/dropwizard/dropwizard/issues/1341#issuecomment-251503011 + environment.environment().jersey() + .property(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, Boolean.TRUE); + + // register client api + environment.modules(new PetStoreApiModule()); + + // optional fake server start + if (environment.configuration().isStartFakeStore()) { + environment.register(FakePetStoreServer.class); + } + } +} +``` + +Registration in app: + +```java +@Override +public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new PetStoreBundle()) + .build()); +} +``` + +## Usage + +```java +@TestDropwizardApp(value = ExampleApp.class, + configOverride = { + "petStoreUrl: http://localhost:8080/fake/petstore", + "startFakeStore: true"}) +public class FakeServerTest { + + @Inject + SampleService sampleService; + + @Test + void testServer() { + + final Pet pet = sampleService.findPet(1); + Assertions.assertNotNull(pet); + Assertions.assertEquals("Jack", pet.getName()); + } +} +``` + diff --git a/dropwizard-guicey/src/doc/docs/guide/test/overview.md b/dropwizard-guicey/src/doc/docs/guide/test/overview.md new file mode 100644 index 000000000..6e8e1e15f --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/test/overview.md @@ -0,0 +1,345 @@ +# Testing + +Guicey provides test extensions for: + +* [JUnit 5](junit5/setup.md) +* [Spock 2](spock2.md) +* [Framework-agnostic utilities](general/general.md) + + +!!! note "Spock 2" + There are no special Spock 2 extensions: junit 5 extensions used directly (with a special library), + so all junit 5 features are available for spock 2. + +Deprecated (because they use +a deprecated [dropwizard rule](https://www.dropwizard.io/en/release-4.0.x/manual/testing.html#junit-4)): + +* [Spock 1](spock.md) +* [JUnit 4](junit4.md) + +Almost all extensions implemented with [DropwizardTestSupport](https://www.dropwizard.io/en/latest/manual/testing.html#non-junit). + +!!! tip + [Test framework-agnostic utilities](general/general.md) could be also used with junit 5 or spock extensions in cases when + assertions required after application shutdown or to test application startup errors. + +## Test concepts + +Dropwziard [proposes atomic testing approach](https://www.dropwizard.io/en/stable/manual/testing.html) (separate testing of each element). + +With DI (guice) we have to move towards **integration testing** because: + +1. It is now harder to mock classes "manually" (because of DI "black box") +2. We have a core (guice injector, without web services), starting much faster than +complete application. + +The following kinds of tests should be used: + +1. Unit tests for atomic parts (usually, utility classes) +2. Business logic (core integration tests): lightweight application starts (without web) to test services + (some services could be mocked or stubbed) +3. Lightweight REST tests: same as 2, but also some rest services simulated (same as + [dropwizard resource testing](https://www.dropwizard.io/en/stable/manual/testing.html#testing-resources)) +4. Web integration tests: full application startup to test web endpoints (full workflow to check transport layer) +5. Custom command tests +6. Application startup fail test (done with command runner) to check self-validations + +## Configuration hooks + +Guicey provides a [hooks mechanism](../hooks.md) for modifying configuration of the existing application. + +Hook receives builder instance used for `GuiceBundle` configuration and so with hook **you can +do everything that could be done is the main bundle** configuration: + +* Register new guice modules +* Register new bundles (dropwizard or guicey) +* Disable extensions/modules/bundles +* Activate guicey reports +* Override guice bindings +* Register some additional extensions (could be useful for validation or to replace existing extension with + a stub implementation) +* Change application options + +Example hook: + +```java +public class MyHook implements GuiceyConfigurationHook { + + public void configure(GuiceBundle.Builder builder) throws Exception { + builder + .disableModules(FeatureXModule.class) + .disable(inPackage("com.foo.feature")) + .modulesOverride(new MockDaoModule()) + .option(Myoptions.DebugOption, true); + } +} +``` + +!!! tip + You can modify [options](../options.md) in hook and so could enable some custom + debug/monitoring options specifically for test. + +## Disables + +Every extension, installed by guicey, **could be disabled**. +When you register extension manually: + +```java +GuiceBundle.builder() + .extensions(MyExceptionMapper.class) + ... +``` + +(or extension detected from classpath scan or from guice binding), guicey controls its installation +and so could avoid registering it (disable). + +!!! note + Guicey **can't** disable extensions registered directly: + ```java + environment.jersey().register(MyExceptionMapper.class) + ``` + +You can use hooks to disable all unnecessary features in test: + +* [installers](../disables.md#disable-installers) +* [extensions](../disables.md#disable-extensions) +* [guice modules](../disables.md#disable-guice-modules) +* [guicey bundles](../disables.md#disable-bundles) +* [dropwizard bundles](../disables.md#disable-dropwizard-bundles) + +This way, you can isolate (as much as possible) some feature for testing. + +The most helpful should be bundles disable (if you use bundles for features grouping) +and guice modules. + +Using [disable predicates](../disables.md#disable-by-predicate) multiple extensions +could be disabled at once (e.g., extensions from some package or only annotated extensions). +But pay attention that predicates applied for all types of extensions! + +## Guice bindings override + +It is a quite common requirement to replace some service with a stub or mock. +That's where guice [overriding bindings](../guice/override.md) come into play (`Modules.override()`). +With it, you don't need to modify existing guice modules to replace any +guice bean for tests (no matter how original binding was declared: it would override +anything, including providers and provider methods). + +For example, we have some service binding declared as: +```java +public class MyModule extends AbstractModule { + + protected configure() { + // assumed @Inject ServiceX used everywhere + bind(ServiceX.class).to(MyServiceX.class); + } +} +``` + +To replace it with `MyServiceXExt`, preparing overriding module: + +```java +public class MyOverridingModule extends AbstractModule { + + protected configure() { + bind(ServiceX.class).to(MyServiceXExt.class); + } +} +``` + +It could be overridden with a hook: + +```java +public class MyHook implements GuiceyConfigurationHook { + public void configure(GuiceBundle.Builder builder) throws Exception { + builder + // the main module is still registered in application: + // .modules(new MyModule()) + .modulesOverride(new MyOverridingModule()); + } +} +``` + +Now all service injections (`@Inject ServiceX`) would receive `MyServiceXExt` instead of `MyServiceX` + +## Debug bundles + +You can also use special guicey bundles, which modify application behavior. +Bundles could contain additional listeners or services to gather additional metrics during +tests or validate behavior. + +For example, guicey tests use a custom bundle to enable restricted guice options like +`disableCircularProxies`: + +```java +public class GuiceRestrictedConfigBundle implements GuiceyBundle { + + @Override + void initialize(GuiceyBootstrap bootstrap) throws Exception { + bootstrap.modules(new GRestrictModule()); + } + + public static class GRestrictModule extends AbstractModule { + @Override + protected void configure() { + binder().disableCircularProxies(); + binder().requireExactBindingAnnotations(); + binder().requireExplicitBindings(); + } + } +} + +// Here bundle is registered with a system property, but +// also could be registered with a hook +PropertyBundleLookup.enableBundles(GuiceRestrictedConfigBundle.class); +``` + +Bundles are less powerful than hooks, but in many cases it is enough, for example to: + +* disable installers, extensions, guice modules +* register custom extensions, modules or bundles +* override guice bindings + +You can also use the [lookup mechanism](../bundles.md#bundle-lookup) to load bundles in tests (like +[system properties lookup](../bundles.md#system-property-lookup) used above). + +## Overriding overridden beans + +Guicey provides [direct support for overriding guice bindings](../guice/override.md), +but this might be already used by application itself (e.g. to "hack" or "patch" some service +in 3rd party module). + +In this case, it would be impossible to override such (already overridden) services and you +should use the provided custom [injector factory](../guice/injector.md#injector-factory): + +Register factory in guice bundle: + +```java +GuiceBundle.builder() + .injectorFactory(new BindingsOverrideInjectorFactory()) +``` + + +After that you can register overriding bindings (which will override even modules registered in `modulesOverride`) +with: + +```java +BindingsOverrideInjectorFactory.override(new MyOverridingModule()) +``` + +!!! important + It is assumed that overriding modules registration and application initialization + will be at the same thread (`ThreadLocal` used for holding registered modules to allow + parallel tests usage). + +For example, suppose we have some service `CustomerService` and it's implementation `CustomerServiceImpl`, +defined in some 3rd party module. For some reason, we need to override this binding in the application: + +```java +public class OverridingModule extends AbstractModule { + @Override + protected void configure() { + bind(CustomerService.class).to(CustomCustomerServiceImpl.class); + } +} +``` + +If we need to override this binding in test (again): + + +```java +public class TestOverridingModule extends AbstractModule { + @Override + protected void configure() { + bind(CustomerService.class).to(CustomServiceStub.class); + } +} +``` + +Registration would look like this: + +```java +GuiceBundle.builder() + .injectorFactory(new BindingsOverrideInjectorFactory()) + .modules(new ThirdPatyModule()) + // override binding for application needs + .modulesOverride(new OverridingModule()) + ... + .build() + +// register overriding somewhere in test +BindingsOverrideInjectorFactory.override(new TestOverridingModule()) +``` + +!!! tip + [Configuration hook](#configuration-hooks) may be used for static call (as a good integration point) + +After test startup, application will use customer service binding from `TestOverridingModule`. + +## Test commands + +Dropwizard commands could be tested with [commands test support](general/command.md) + +For example: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .run("simple", "-u", "user"); + +Assertions.assertTrue(result.isSuccessful()); +``` + +There are no special junit 5 extensions for command tests because direct run method is +already the best way. + +!!! warning + Commands support override `System.in/out/err` to collect all output (and control input) + which makes such tests impossible to run in parallel. + +## Test application startup fail + +To verify application self-validation mechanisms (make sure application would fail with incomplete configuration, +or whatever another reason) use [commands runner](general/startup.md). + +For example: + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .runApp(); + +Assertions.assertEquals(); +``` + +!!! note "Why not run directly?" + You can run command directly: `new App().run("simple", "-u", "user")` + But, if application throws exception in *run* phase, `System.exit(1)` would be called: + + ```java + public abstract class Application { + ... + protected void onFatalError(Throwable t) { + System.exit(1); + } + } + ``` + Commands runner runs commands directly so exit would not be called. + +## Configuration + +In tests, you can either use a custom configuration file with config overrides +or create configuration instance manually. + +For modifying configuration, there are two mechanisms: + +* Configuration overrides: dropwizard mechanism works only with file-based configuration. + Works through system properties and may not work in some cases (for example, for collection + properties) +* Configuration modifiers: guicey mechanism allowing direct configuration modification before + application startup (works with file-based configuration and manually created configuration + instance) + +## Web client + +Guicey provides a [ClientSupport](general/client.md) instance which provides basic methods for calling +web endpoints. The most helpful part of it is that it will self-configure automatically, +according to the provided configuration, so you don't need to modify tests +when rest or admin mapping changes. diff --git a/src/doc/docs/guide/test/spock.md b/dropwizard-guicey/src/doc/docs/guide/test/spock.md similarity index 97% rename from src/doc/docs/guide/test/spock.md rename to dropwizard-guicey/src/doc/docs/guide/test/spock.md index 35880857d..debc043c9 100644 --- a/src/doc/docs/guide/test/spock.md +++ b/dropwizard-guicey/src/doc/docs/guide/test/spock.md @@ -4,7 +4,7 @@ groovy language. !!! warning - Since guicey 5.5 spock 1 support was extracted from guicey to [external module](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-test-spock). + Since guicey 5.5 spock 1 support was extracted from guicey to [external module](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-test-spock). Package remains the same to simplify migration (only additional dependency would be required) This was required because spock 1 does not work on JDK 16 and above. @@ -125,7 +125,7 @@ more resides in root). When used with existing configuration file, this paramete ## Guice injections -Any gucie bean may be injected directly into test field: +Any guice bean may be injected directly into test field: ```groovy @Inject @@ -210,7 +210,7 @@ Example usages: client.targetRest("some").request().buildGet().invoke() // GET {main context path}/servlet -client.targetMain("servlet").request().buildGet().invoke() +client.targetApp("servlet").request().buildGet().invoke() // GET {admin context path}/adminServlet client.targetAdmin("adminServlet").request().buildGet().invoke() @@ -240,7 +240,7 @@ Also, if you want to use other client, client object can simply provide required ```groovy client.getPort() // app port (8080) client.getAdminPort() // app admin port (8081) -client.basePathMain() // main context path (http://localhost:8080/) +client.basePathApp() // main context path (http://localhost:8080/) client.basePathAdmin() // admin context path (http://localhost:8081/) client.basePathRest() // rest context path (http://localhost:8080/) ``` @@ -501,7 +501,7 @@ Note that `server.rootPath` could be configured with `restMapping` annotation pr ### Alternative declaration -You may also use [alternative declaration](junit5.md#alternative-declaration): +You may also use [alternative declaration](junit5/run.md#alternative-declaration): ```groovy class MyTest extends Specification { diff --git a/src/doc/docs/guide/test/spock2.md b/dropwizard-guicey/src/doc/docs/guide/test/spock2.md similarity index 67% rename from src/doc/docs/guide/test/spock2.md rename to dropwizard-guicey/src/doc/docs/guide/test/spock2.md index 9dfa9c4c5..7bada47c2 100644 --- a/src/doc/docs/guide/test/spock2.md +++ b/dropwizard-guicey/src/doc/docs/guide/test/spock2.md @@ -3,9 +3,9 @@ !!! note "" [Migration from spock 1](spock.md#migration-to-spock-2) -There is no special extensions for [Spock 2](http://spockframework.org) (like it was for spock 1), +There are no special extensions for [Spock 2](http://spockframework.org) (like it was for spock 1), instead I did an extra [integration library](https://github.com/xvik/spock-junit5), -so you can use existing [Junit 5 extensions](junit5.md) with spock. +so you can use existing [Junit 5 extensions](junit5/setup.md) with spock. !!! note You are not limited to guicey junit 5 extensions, you can use ([almost](https://github.com/xvik/spock-junit5#what-is-supported)) any junit 5 extensions. @@ -18,7 +18,7 @@ You will need the following dependencies (assuming BOM used for versions managem ```groovy testImplementation 'ru.vyarus:spock-junit5' -testImplementation 'org.spockframework:spock-core:2.1-groovy-3.0' +testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation 'io.dropwizard:dropwizard-testing' testImplementation 'org.junit.jupiter:junit-jupiter-api' ``` @@ -34,7 +34,7 @@ testImplementation 'org.junit.jupiter:junit-jupiter-api' ## Usage -See [junit 5 extensions docs](junit5.md) for usage details (it's all used the same). +See [junit 5 extensions docs](junit5/setup.md) for usage details (it's all used the same). !!! warning Junit 5 extensions would not work with `@Shared` spock fields! You can still use @@ -64,14 +64,42 @@ class MyTest extends Specification { ``` !!! tip - Note that [parameter injection](junit5.md#parameter-injection) will also work in test and fixture (setup/cleanup) methods + Note that [parameter injection](junit5/inject.md#parameter-injection) will also work in test and fixture (setup/cleanup) methods Overall, you get best of both worlds: same extensions as in junit 5 (and ability to use all other junit extensions) and spock expressiveness for writing tests. +## Testing commands + +!!! warning + Commands execution overrides System IO and so can't run in parallel with other tests! + + Use [`@Isolated`](https://spockframework.org/spock/docs/2.0/all_in_one.html#_isolated_execution) + on such tests to prevent parallel execution with other tests + +Command execution is usually a short-lived action, so it is not possible to +write an extension for it. Command could be tested only with generic utility: + +```java +def "Test command execution"() { + + when: "executing command" + CommandResult result = TestSupport.buildCommandRunner(App) + .run("cmd", "-p", "param") + + then: "success" + result.successful +} +``` + +Read more details in [junit 5 guide](junit5/command.md) + +!!! note + The same utility could be used to test [application startup fails](junit5/startup.md) + ## Special cases -Junit 5 doc [describes](junit5.md#dropwizard-startup-error) [system stubs](https://github.com/webcompere/system-stubs) library +Junit 5 doc [describes](junit5/startup.md) [system stubs](https://github.com/webcompere/system-stubs) library usage. It is completely valid for spock, I'll just show a few examples here on how to: * Modify (and reset) environment variables @@ -128,5 +156,5 @@ class EnvironmentChangeTest extends Specification { ``` !!! note - Use [test framework-agnostic utilities](general.md) to run application with configuration or to run + Use [test framework-agnostic utilities](general/general.md) to run application with configuration or to run application without web part (for faster test). diff --git a/dropwizard-guicey/src/doc/docs/guide/url-builder.md b/dropwizard-guicey/src/doc/docs/guide/url-builder.md new file mode 100644 index 000000000..71e5327a5 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/guide/url-builder.md @@ -0,0 +1,77 @@ +# Application URLs builder + +Sometimes, it is required to build application url: for example, generate email link, or use it for redirect. +But server configuration could be changed, affecting target url: + +* Default or simple server (difference for admin port) +* Port +* Contexts: + - server.applicationContextPath - application context + - server.rootPath - rest context + - server.adminContextPath - admin context + +There is a `AppUrlBuilder` class for building links with automatic resolution of +the current server configuration (the same as `ClientSupport`). + +Also, mechanism, used in [resource clients](test/general/client.md#resource-clients), +could be also used to build application urls in a type-safe manner (for rest methods). + +`AppUrlBuilder` is not bound by default in the guice context, but could be injected (as jit binding): + +```java +@Inject AppUrlBuilder builder; +``` + +Or it could be created manually: + +```java +AppUrlBuilder builder = new AppUrlBuilder(environment); +``` + +There are 3 scenarios: + +* Localhost urls: the default mode when all urls contain "localhost" and application port. +* Custom host: `builder.forHost("myhost.com")` when custom host used instead of localhost and application port + is applied automatically +* Proxy mode: `builder.forProxy("https://myhost.com")` when application is behind some proxy + (like apache or nginx) hiding its real port. + +Examples: + +```java +// http://localhost:8080/ +builder.root("/") +// http://localhost:8080/ +builder.app("/") +// http://localhost:8081/ +builder.admin("/") +// http://localhost:8080/ +builder.rest("/") + +// http://localhost:8080/users/123 +builde.rest(Resource.class).method(r -> r.get(123)).build() +// http://localhost:8080/users/123 +builde.rest(Resource.class).method(r -> r.get(null)).pathParam("id", 123).build() + + +// https://some.com:8081/something +builder.forHost("https://some.com").admin("/something") + +// https://some.com/something +builder.forProxy("https://some.com").admin("/something") +``` + +`AppUrlBuilder` could be used to get access to current server configuration: + +```java +// 8080 +builder.getAppPort(); +// 8081 +builder.getAdminPort(); +// "/" (server.adminContextPath) +builder.getAdminContextPath(); +// "/" (server.applicationContextPath) +builder.getAppContextPath(); +// "/" (server.rootPath) +builder.getRestContextPath(); +``` \ No newline at end of file diff --git a/src/doc/docs/guide/web.md b/dropwizard-guicey/src/doc/docs/guide/web.md similarity index 94% rename from src/doc/docs/guide/web.md rename to dropwizard-guicey/src/doc/docs/guide/web.md index ad283999c..4b3c6a716 100644 --- a/src/doc/docs/guide/web.md +++ b/dropwizard-guicey/src/doc/docs/guide/web.md @@ -36,7 +36,7 @@ public class WebModule extends ServletModule { ### Web extensions -Extensions declared with standard `javax.servlet` annotations. +Extensions declared with standard `jakarta.servlet` annotations. Servlet registration: @@ -84,7 +84,7 @@ Extension [recognized](../installers/listener.md) by `@WebListener` annotation. * Filter may be applied to exact servlet(s) (`#!java @WebFilter(servletNames = "servletName")`) * Request, servlet context or session [listeners installation](../installers/listener.md) -If you don't want to use web installers or have problems with it (e.g. because they use `javax.servlet` annotations) +If you don't want to use web installers or have problems with it (e.g. because they use `jakarta.servlet` annotations) you can disable all of them at once by disabling bundle: ```java @@ -113,7 +113,7 @@ public class App extends Application { ## Resources -Dropwizard provides [AssetsBundle](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#serving-assets) +Dropwizard provides [AssetsBundle](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#serving-assets) for serving static files from classpath: ```java @@ -138,14 +138,14 @@ GuiceBundle.builder() ## Templates -Dropwizard provides [ViewBundle](https://www.dropwizard.io/en/release-2.0.x/manual/views.html) +Dropwizard provides [ViewBundle](https://www.dropwizard.io/en/release-4.0.x/manual/views.html) for handling templates (freemarker and mustache out of the box, more engines could be plugged). ```java bootstrap.addBundle(new ViewBundle()); ``` -Which allows you to serve rendered templates [from rest endpoints](https://www.dropwizard.io/en/release-2.0.x/manual/views.html). +Which allows you to serve rendered templates [from rest endpoints](https://www.dropwizard.io/en/release-4.0.x/manual/views.html). ### Templates + resources @@ -264,8 +264,6 @@ Index page: to be able to differentiate rest for different UI applications !!! warning - Standard errors handling in views ([templates](https://www.dropwizard.io/en/release-2.0.x/manual/views.html#template-errors), - [custome pages](https://www.dropwizard.io/en/release-2.0.x/manual/views.html#custom-error-pages)) is replaced by + Standard errors handling in views ([templates](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#template-errors), + [custom pages](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#custom-error-pages)) is replaced by [custom mechanism](../extras/gsp.md#error-pages), required to implement per-ui-app errors support. - - \ No newline at end of file diff --git a/src/doc/docs/guide/yaml-values.md b/dropwizard-guicey/src/doc/docs/guide/yaml-values.md similarity index 61% rename from src/doc/docs/guide/yaml-values.md rename to dropwizard-guicey/src/doc/docs/guide/yaml-values.md index d751ec742..f73a54d8f 100644 --- a/src/doc/docs/guide/yaml-values.md +++ b/dropwizard-guicey/src/doc/docs/guide/yaml-values.md @@ -1,18 +1,19 @@ # Yaml values -Guicey introspects `Configuration` object instance using jackson serialization api to allow accessing yaml configuration -values by yaml paths or sub-object types. +Guicey introspects `Configuration` object instance using jackson serialization api to allow direct access to yaml configuration +values. -Introspected configuration: +Introspected configuration is accessible as: -* Bound directly in guice to inject direct values (by path) or sub objects -* Accessible from guice modules (with help of `ConfigurationAwareModule` interface) and bundles. +* Direct guice bindigs for unique sub objects, and all properties +* Bindings for manually qualified properties +* `ConfigurationTree` object (binding) containing all introspection data (could be used for manual searches and, for example, for reports) +* Guice modules and guicey bundles could access introspected configuration with help of `ConfigurationAwareModule` interface +and `GuiceyEnvironment` object. -In both cases raw introspection result object is accessible: `ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree`. -It could be used for config analysis, reporting or something else. !!! warning - Jackson will see all properties which either have getter and setter or annotated with `@JsonProperty`. For example, + Jackson will see all properties that either have getter and setter or annotated with `@JsonProperty`. For example, ```java public class MyConfig extends Configuration { @@ -44,6 +45,12 @@ It could be used for config analysis, reporting or something else. public String getProp() { ... } ``` +!!! important + Guice [does not allow null value bindings](https://github.com/google/guice/wiki/NULL_INJECTED_INTO_NON_NULLABLE) + by default, so if you bind configuration property with null value injector creation would fail. + + To workaround it, use `@jakarta.inject.Nullable` for injected field. + ## Unique sub configuration It is quite common to group configuration properties into sub objects like: @@ -96,10 +103,93 @@ public class FeatureXService { !!! tip Guicey bundles and guice modules also could use sub configuration objects directly: ```java - GuiceyBootstrap#configuration(SubConfig.class) + GuiceyEnvironment#configuration(SubConfig.class) DropwizardAwareModule#configuration(SubConfig.class) ``` +## Qualified bindings + +Automatic unique objects bindings require using guicey binding annotation. +In some cases, this is not possible: for example, generic 3rd party guice module. +You can use qualified bindings then. + +The idea is simple: just annotate any configuration property or getter with a qualified +annotation and it would be bound in guice context. Moreover, if multiple properties +(same type!) would be annotated with the same annotation - they would be bound as `Set`. + +For example, + +```java +public class MyConfig extends Configuration { + + @Named("custom") + private String prop1; + + @CustomQualifier + private SubObj obj1 = new SubObj(); +``` + +!!! tip + Custom qualifying annotation must be annotated with guice `@BindingAnnotation` + or jakarta `@Qualifier` (see guice and jakarta `@Named` annotations as an example). + + ```java + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) + @BindingAnnotation + public @interface CustomQualifier {} + ``` + +Would mean two additional bindings: + +```java +@Inject @Named("custom") String prop1; +@Inject @CustomQualifier SubObj obj1; +``` + +!!! important + If you expect null values then identify it (otherwise injector creation would fail for null value): + `@Inject @Named("custom") @jakarta.inject.Nullable String prop1;` + +And binding like this: + +```java +@Named("sub-prop") +private String prop2; +@Named("sub-prop") +private String prop3; +``` + +Would result in aggregated binding: + +```java +@Inject @Named("sub-prop") Set prop23; +``` + +!!! note + Qualifying annotations may be used on any configuration depth (not only in the root + configuration object). + +Core dropwizard objects could also be bound with a qualified overridden getter: + +```java +@Named("metrics") +@Override +MetricsFactory getMetricsFactory() { + return super.getMetricsFactory(); +} +``` + +!!! note + All custom bindings are visible in the configuration report: [`.printCustomConfigurationBindings()`](diagnostic/configuration-report.md) + +!!! tip + Guicey bundles and guice modules also could use qualified configuration values directly: + ```java + GuiceyEnvironment#annotatedConfiguration(Ann.class) + DropwizardAwareModule#annotatedConfiguration(Ann.class) + ``` + ## Configuration by path All visible configuration paths values are directly bindable: @@ -124,7 +214,7 @@ public class SubConf { !!! note Path bindings are available even for null values. For example, if sub configuration object is null, all it's sub paths will still be available (by class declarations). - The only exception is conditional mappin like dropwizard "server" when available paths + The only exception is conditional mapping like dropwizard "server" when available paths could change, depending on configuration (what configuration class will be used) !!! note @@ -187,7 +277,24 @@ You can traverse up or down from any path (tree structure). * `valueByPath(String)` - return path value or null if value null or path not exists * `valuesByType(Class)` - all not null values with assignable type * `valueByType(Class)` - first not null value with assignable type -* `valueByUniqueDeclaredType(Class)` - value of unique sub conifguration or null if value is null or config is not unique +* `valueByUniqueDeclaredType(Class)` - value of unique sub configuration or null if value is null or config is not unique + +Methods to work with qualified configurations: + +* `findAllByAnnotation(ann | Class)` - find all annotated properties by annotation instance or annotation class +* `findByAnnotation(ann | Class)` - find exactly one annotated property or throw error if more then one found +* `annotatatedValues(ann | Class)` - all non-null values from annotated properties +* `annotatatedValue(ann | Class)` - an annotated property value (throw error if more properties annotated) + +!!! tip + Searching by annotation instance if required for annotations with "state". For example, + `@Named("something")` and `@Named("other")` are different qualifying annotations, + and searching only by type would be incorrect in this case. + + It is not possible to directly create annotation instance, but any annotation + is and interface and could be implemented. It is important that real annotation + (from class) and provided object would be equal. As an example see how guice `Names.named()` + consrtructs "@Named" insatnces. Paths are sorted by configuration class (to put custom properties upper) and by path name (for predictable paths order). diff --git a/dropwizard-guicey/src/doc/docs/img/jvm/jvm1.webp b/dropwizard-guicey/src/doc/docs/img/jvm/jvm1.webp new file mode 100644 index 000000000..87fa7f333 Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/jvm/jvm1.webp differ diff --git a/dropwizard-guicey/src/doc/docs/img/jvm/jvm2.webp b/dropwizard-guicey/src/doc/docs/img/jvm/jvm2.webp new file mode 100644 index 000000000..556b93081 Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/jvm/jvm2.webp differ diff --git a/dropwizard-guicey/src/doc/docs/img/jvm/jvm3.webp b/dropwizard-guicey/src/doc/docs/img/jvm/jvm3.webp new file mode 100644 index 000000000..6e0a2c492 Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/jvm/jvm3.webp differ diff --git a/dropwizard-guicey/src/doc/docs/img/jvm/jvm4.webp b/dropwizard-guicey/src/doc/docs/img/jvm/jvm4.webp new file mode 100644 index 000000000..5d1c355f2 Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/jvm/jvm4.webp differ diff --git a/dropwizard-guicey/src/doc/docs/img/sponsors/channel.png b/dropwizard-guicey/src/doc/docs/img/sponsors/channel.png new file mode 100644 index 000000000..eddc63539 Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/sponsors/channel.png differ diff --git a/dropwizard-guicey/src/doc/docs/img/sponsors/channel2.png b/dropwizard-guicey/src/doc/docs/img/sponsors/channel2.png new file mode 100644 index 000000000..772130e0f Binary files /dev/null and b/dropwizard-guicey/src/doc/docs/img/sponsors/channel2.png differ diff --git a/src/doc/docs/img/sponsors/zoyi-ch.png b/dropwizard-guicey/src/doc/docs/img/sponsors/zoyi-ch.png similarity index 100% rename from src/doc/docs/img/sponsors/zoyi-ch.png rename to dropwizard-guicey/src/doc/docs/img/sponsors/zoyi-ch.png diff --git a/dropwizard-guicey/src/doc/docs/index.md b/dropwizard-guicey/src/doc/docs/index.md new file mode 100644 index 000000000..6860a4695 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/index.md @@ -0,0 +1,85 @@ +# Welcome to dropwizard-guicey + +!!! summary "" + [Guice](https://github.com/google/guice) `{{ gradle.guice }}` integration for [dropwizard](http://dropwizard.io) `{{ gradle.dropwizard }}`. + Compiled for `java 11`, compatible with `java 11 - 21`. + +**[Release Notes](about/release-notes.md)** - [History](about/history.md) - [Javadoc](https://javadoc.io/doc/ru.vyarus/dropwizard-guicey/) - [Support](about/support.md) - [License](about/license.md) + +!!! note "" + For migration see [migration guide](about/migration.md) + +## Main features + +* Auto configuration from [classpath scan](guide/scan.md) and [guice bindings](guide/guice/module-analysis.md#extensions-recognition). +* [Yaml config values bindings](guide/yaml-values.md) by path or unique sub objects. +* Advanced [Web support](guide/web.md) +* Dropwizard style [console reporting](guide/installers.md#reporting): detected (and installed) extensions are printed to console to remove uncertainty +* [Test support](guide/test/overview.md): custom junit and [spock](http://spockframework.org) extensions + - Advanced test abilities to [disable](guide/disables.md) or [override](guide/guice/override.md) application logic +* Developer friendly: + - core integrations [may be replaced](guide/disables.md#disable-installers) (to better fit needs) + - rich api for developing [custom integrations](guide/installers.md#writing-custom-installer), and hooking into [lifecycle](guide/events.md)) + - out of the box support for plug-n-play plugins ([auto discoverable](guide/bundles.md#service-loader-lookup)) + - [diagnostic tools](guide/diagnostic/diagnostic-tools.md) (reports), support for [custom diagnostic tools](guide/hooks.md#diagnostic) + +## Sponsors + +: [![Channel Talk](img/sponsors/channel2.png)](https://channel.io "Channel Talk") + + +If guicey makes your life easier, you can [support its development](https://www.patreon.com/guicey). + +## Project structure + +* [ru.vyarus:dropwizard-guicey](https://github.com/xvik/dropwizard-guicey/tree/master/dropwizard-guicey) - core + guicey module. Could be used without any extra modules +* [ru.vyarus.guicey:guicey-[module name]](https://github.com/xvik/dropwizard-guicey) - guicey extension + [modules](guide/modules.md) (use with `ru.vyarus.guicey:guicey-bom`). Modules provide additional functionality like + 3rd party libraries integration. Also, serve as an example of possible extension implementations. +* [Examples](https://github.com/xvik/dropwizard-guicey/tree/master/examples) - various usage examples for core guicey, + extension modules and some direct integrations + +!!! note "" + Before, guicey and extensions were released separately in different repositories - different packages were preserved after merge + +## Supported versions + +Due to 3 major changes in dropwizard recently, 3 guicey versions supported: + +Dropwizard | Guicey | Reason +----------|--------------------------------------------------------------|------- +2.1.x| [5.x](https://github.com/xvik/dropwizard-guicey/tree/dw-2.1) | Last java 8 compatible version +3.x | [6.x](https://github.com/xvik/dropwizard-guicey/tree/dw-3) | [Changed core dropwizard packages](https://github.com/dropwizard/dropwizard/blob/release/3.0.x/docs/source/manual/upgrade-notes/upgrade-notes-3_0_x.rst) - old 3rd paty bundles would be incompatible +4.x | 7.x | [Jakarta namespace migration](https://github.com/dropwizard/dropwizard/blob/release/4.0.x/docs/source/manual/upgrade-notes/upgrade-notes-4_0_x.rst) - 3rd party guice modules might be incompatible + +All branches use the same project structure: core guicey merged with extension modules. +It greatly simplifies releases and keeps actual examples in one branch. + +Upcoming guicey changes would be ported in all 3 branches. + +## SBOM + +[SBOM (cyclonedx)](https://cyclonedx.org/) is published for every guicey module with `cyclonedx` classifier (same way as dropwizard) +as json and xml files. + +For example: [XML](https://repo1.maven.org/maven2/ru/vyarus/dropwizard-guicey/{{ gradle.version }}/dropwizard-guicey-{{ gradle.version }}-cyclonedx.xml), +[JSON](https://repo1.maven.org/maven2/ru/vyarus/dropwizard-guicey/{{ gradle.version }}/dropwizard-guicey-{{ gradle.version }}-cyclonedx.json) + +## Documentation Summary + +### Introduction + +* [**Getting started**](getting-started.md) guide describes installation and provides core usage examples +* [**Concepts overview**](concepts.md) guide introduces core guicey concepts and demonstrates differences from pure dropwizard usage +* [**Guice**](guice.md) the essence of guice integration +* [**Testing**](tests.md) describes integration testing techniques +* [**Decomposition**](decomposition.md) guide on writing re-usable modules + +### Reference +* [**User guide**](guide/configuration.md) contains detailed feature descriptions. It is good to read, but it also functions + well as a reference if you're short on time. +* [**Installers**](installers/resource.md) describes all guicey installers. Use it as a *extensions hand book*. +* [**Modules**](guide/modules.md) external extension modules overview. +* [**Examples**](examples/authentication.md) some usage examples. +* [Standalone sample application](https://github.com/xvik/dropwizard-app-todo) diff --git a/src/doc/docs/installers/eager.md b/dropwizard-guicey/src/doc/docs/installers/eager.md similarity index 90% rename from src/doc/docs/installers/eager.md rename to dropwizard-guicey/src/doc/docs/installers/eager.md index c7eb86100..5f012c906 100644 --- a/src/doc/docs/installers/eager.md +++ b/dropwizard-guicey/src/doc/docs/installers/eager.md @@ -5,10 +5,10 @@ ## Recognition -Detects classes annotated with `#!java @EagerSingleton` annotation and register them in guice injector. +Detects classes annotated with `#!java @EagerSingleton` annotation and registers them in guice injector. It is equivalent of eager singleton registration `#!java bind(type).asEagerSingleton()`. -Useful in case when you have bean not injected by other beans (so guice can't register +Useful in cases when you have a bean which is not injected by other beans (so guice can't register it through aot). Normally, you would have to manually register such bean in module. Most likely, such bean will contain initialization logic. @@ -30,4 +30,3 @@ Class will be recognized by eager singleton installer, environment object inject May be used in conjunction with `#!java @PostConstruct` annotations (e.g. using [ext-annotations](https://github.com/xvik/guice-ext-annotations)): installer finds and register bean and post construct annotation could run some logic. Note: this approach is against guice philosophy and should be used for quick prototyping only. - diff --git a/src/doc/docs/installers/filter.md b/dropwizard-guicey/src/doc/docs/installers/filter.md similarity index 95% rename from src/doc/docs/installers/filter.md rename to dropwizard-guicey/src/doc/docs/installers/filter.md index 9aeb19120..3ffa4143e 100644 --- a/src/doc/docs/installers/filter.md +++ b/dropwizard-guicey/src/doc/docs/installers/filter.md @@ -7,7 +7,7 @@ Register new filter in main or admin contexts. ## Recognition -Detects classes annotated with `@javax.servlet.annotation.WebFilter` annotation and register them in dropwizard environment. +Detects classes annotated with `@jakarta.servlet.annotation.WebFilter` annotation and register them in dropwizard environment. ```java @WebFilter("/some/*") diff --git a/src/doc/docs/installers/healthcheck.md b/dropwizard-guicey/src/doc/docs/installers/healthcheck.md similarity index 95% rename from src/doc/docs/installers/healthcheck.md rename to dropwizard-guicey/src/doc/docs/installers/healthcheck.md index faf3ab284..813f0d61f 100644 --- a/src/doc/docs/installers/healthcheck.md +++ b/dropwizard-guicey/src/doc/docs/installers/healthcheck.md @@ -3,7 +3,7 @@ !!! summary "" CoreInstallersBundle / [HealthCheckInstaller](https://github.com/xvik/dropwizard-guicey/tree/master/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java) -Installs [dropwizard health check](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#health-checks). +Installs [dropwizard health check](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#health-checks). ## Recognition diff --git a/src/doc/docs/installers/jersey-ext.md b/dropwizard-guicey/src/doc/docs/installers/jersey-ext.md similarity index 71% rename from src/doc/docs/installers/jersey-ext.md rename to dropwizard-guicey/src/doc/docs/installers/jersey-ext.md index 3f60c5ef1..095ddacb7 100644 --- a/src/doc/docs/installers/jersey-ext.md +++ b/dropwizard-guicey/src/doc/docs/installers/jersey-ext.md @@ -8,18 +8,26 @@ Installs various jersey extensions, usually annotated with jersey `#!java @Provi Supplier, ExceptionMapper, ValueParamProvider, InjectionResolver, ParamConverterProvider, ContextResolver, MessageBodyReader, MessageBodyWriter, ReaderInterceptor, WriterInterceptor, ContainerRequestFilter, - ContainerResponseFilter, DynamicFeature, ApplicationEventListener + ContainerResponseFilter, DynamicFeature, ApplicationEventListener, ModelProcessor ## Recognition -Detects classes annotated with jersey `@javax.ws.rs.ext.Provider` annotation and register their instances in jersey. +Detects known jersey extension classes and classes annotated with jersey `@jakarta.ws.rs.ext.Provider` annotation and register their instances in jersey. !!! attention "" Extensions registered as **singletons**, when no explicit scope annotation is used. Behaviour could be disabled with [option](../guide/options.md): ```java .option(InstallerOptions.ForceSingletonForJerseyExtensions, false) - ``` + ``` + +!!! tip "" + Before guicey 5.7.0 it was required to annotate all extensions with `@Provide`, but now + it is not required - extension would be recognized by implemented interface. + But, if you prefer legacy behaviour then it could be reverted with: + ```java + .option(InstallersOptions.JerseyExtensionsRecognizedByType, false) + ``` Special `@Prototype` scope annotation may be used to mark resources in prototype scope. It is useful when [guice servlet support is disabled](../guide/web.md#disable-servletmodule-support) (and so `@RequestScoped` could not be used). @@ -28,7 +36,7 @@ Due to specifics of [HK2 integration](lifecycle.md), you may need to use: * `#!java @JerseyManaged` to delegate bean creation to HK2 * `#!java @LazyBinding` to delay bean creation to time when all dependencies will be available -* `javax.inject.Provider` as universal workaround (to wrap not immediately available dependency). +* `jakarta.inject.Provider` as universal workaround (to wrap not immediately available dependency). Or you can enable [HK2 management for jersey extensions by default](../guide/hk2.md#use-hk2-for-jersey-extensions). Note that this will affect [resources](resource.md) too and guice aop will not work on jersey extensions. @@ -56,7 +64,7 @@ org.glassfish.jersey.internal.inject.InjectionManager, java.lang.Class)` which i `@Custom` may be used directly in this case on some providers for prioritization. `@Priority` annotation may be used for ordering providers. Value should be > 0 (but may be negative, just a convention). -For example, 1000 is more prioritized then 2000. See `javax.ws.rs.Priorities` for default priority constants. +For example, 1000 is prioritized before 2000. See `jakarta.ws.rs.Priorities` for default priority constants. !!! note `@Priority` may work differently on `@Custom` qualified providers (all user providers by default) @@ -67,7 +75,7 @@ For example, 1000 is more prioritized then 2000. See `javax.ws.rs.Priorities` fo ### Supplier !!! warning - `Supplier` is used now by hk2 as a replacement to it's own `Factory` interface. + `Supplier` is used now by hk2 as a replacement to its own `Factory` interface. If you were using `AbstractContainerRequestValueFactory` then use just `Supplier` instead. @@ -83,7 +91,7 @@ public class MySupplier implements Supplier { ``` !!! tip "" - Suppliers in essence are very like guice (or `javax.inject`) providers (`#!java Provider`). + Suppliers in essence are very like guice (or `jakarta.inject`) providers (`#!java Provider`). !!! warning Previously, factories were used as auth objects providers. Now `Function` must be used instead: @@ -101,11 +109,10 @@ public class MySupplier implements Supplier { ### ExceptionMapper -Any class implementing `#!java javax.ws.rs.ext.ExceptionMapper` (or extending abstract class implementing it). -Useful for [error handling customization](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#error-handling). +Any class implementing `#!java jakarta.ws.rs.ext.ExceptionMapper` (or extending abstract class implementing it). +Useful for [error handling customization](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#error-handling). ```java -@Provider public class DummyExceptionMapper implements ExceptionMapper { private final Logger logger = LoggerFactory.getLogger(DummyExceptionMapper.class); @@ -124,11 +131,11 @@ public class DummyExceptionMapper implements ExceptionMapper { !!! tip You can also use `ExtendedExceptionMapper` as more flexible alternative. See example usage in - [dropwizard-views](https://www.dropwizard.io/en/release-2.0.x/manual/views.html#template-errors). + [dropwizard-views](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#template-errors). !!! tip Default exception dropwizard mappers (registered in `io.dropwizard.setup.ExceptionMapperBinder`) could be - [overridden](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#overriding-default-exception-mappers) + [overridden](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#overriding-default-exception-mappers) (see [priority section](#priority)) or completely disabled with `server.registerDefaultExceptionMappers` option. @@ -137,13 +144,12 @@ public class DummyExceptionMapper implements ExceptionMapper { Any class implementing `#!java org.glassfish.jersey.server.spi.internal.ValueParamProvider` (or extending abstract class implementing it). ```java -@Provider public class AuthFactoryProvider extends AbstractValueParamProvider { private final AuthFactory authFactory; @Inject - public AuthFactoryProvider(final javax.inject.Provider extractorProvider, + public AuthFactoryProvider(final jakarta.inject.Provider extractorProvider, final AuthFactory factory) { super(extractorProvider, Parameter.Source.UNKNOWN); this.authFactory = factory; @@ -162,7 +168,6 @@ public class AuthFactoryProvider extends AbstractValueParamProvider { Any class implementing `#!java org.glassfish.hk2.api.InjectionResolver` (or extending abstract class implementing it). ```java -@Provider class MyObjInjectionResolver implements InjectionResolver { @Override @@ -189,10 +194,9 @@ class MyObjInjectionResolver implements InjectionResolver { ### ParamConverterProvider -Any class implementing [`#!java javax.ws.rs.ext.ParamConverterProvider`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/ParamConverterProvider.html) (or extending abstract class implementing it). +Any class implementing [`#!java jakarta.ws.rs.ext.ParamConverterProvider`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/paramconverterprovider) (or extending abstract class implementing it). ```java -@Provider public class FooParamConverter implements ParamConverterProvider { @Override @@ -219,10 +223,9 @@ public class FooParamConverter implements ParamConverterProvider { ### ContextResolver -Any class implementing [`#!java javax.ws.rs.ext.ContextResolver`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/ContextResolver.html) (or extending abstract class implementing it). +Any class implementing [`#!java jakarta.ws.rs.ext.ContextResolver`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/contextresolver) (or extending abstract class implementing it). ```java -@Provider public class MyContextResolver implements ContextResolver { @Override @@ -236,11 +239,10 @@ public class MyContextResolver implements ContextResolver { ### MessageBodyReader -Any class implementing [`#!java javax.ws.rs.ext.MessageBodyReader`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/MessageBodyReader.html) (or extending abstract class implementing it). -Useful for [custom representations](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#custom-representations). +Any class implementing [`#!java jakarta.ws.rs.ext.MessageBodyReader`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/messagebodyreader) (or extending abstract class implementing it). +Useful for [custom representations](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#custom-representations). ```java -@Provider public class TypeMessageBodyReader implements MessageBodyReader { @Override @@ -259,11 +261,10 @@ public class TypeMessageBodyReader implements MessageBodyReader { ### MessageBodyWriter -Any class implementing [`#!java javax.ws.rs.ext.MessageBodyWriter`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/MessageBodyWriter.html) (or extending abstract class implementing it). -Useful for [custom representations](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#custom-representations). +Any class implementing [`#!java jakarta.ws.rs.ext.MessageBodyWriter`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/messagebodywriter) (or extending abstract class implementing it). +Useful for [custom representations](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#custom-representations). ```java -@Provider public class TypeMessageBodyWriter implements MessageBodyWriter { @Override @@ -286,10 +287,9 @@ public class TypeMessageBodyWriter implements MessageBodyWriter { ### ReaderInterceptor -Any class implementing [`#!java javax.ws.rs.ext.ReaderInterceptor`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/ReaderInterceptor.html) (or extending abstract class implementing it). +Any class implementing [`#!java jakarta.ws.rs.ext.ReaderInterceptor`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/readerinterceptor) (or extending abstract class implementing it). ```java -@Provider public class MyReaderInterceptor implements ReaderInterceptor { @Override @@ -301,10 +301,9 @@ public class MyReaderInterceptor implements ReaderInterceptor { ### WriterInterceptor -Any class implementing [`#!java javax.ws.rs.ext.WriterInterceptor`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/ext/WriterInterceptor.html) (or extending abstract class implementing it). +Any class implementing [`#!java jakarta.ws.rs.ext.WriterInterceptor`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/ext/writerinterceptor) (or extending abstract class implementing it). ```java -@Provider public class MyWriterInterceptor implements WriterInterceptor { @Override @@ -315,11 +314,10 @@ public class MyWriterInterceptor implements WriterInterceptor { ### ContainerRequestFilter -Any class implementing [`#!java javax.ws.rs.container.ContainerRequestFilter`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/container/ContainerRequestFilter.html) (or extending abstract class implementing it). -Useful for [request modifications](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#jersey-filters). +Any class implementing [`#!java jakarta.ws.rs.container.ContainerRequestFilter`](https://jakarta.ee/specifications/platform/9/apidocs/jakarta/ws/rs/container/containerrequestfilter) (or extending abstract class implementing it). +Useful for [request modifications](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#jersey-filters). ```java -@Provider public class MyContainerRequestFilter implements ContainerRequestFilter { @Override @@ -330,11 +328,10 @@ public class MyContainerRequestFilter implements ContainerRequestFilter { ### ContainerResponseFilter -Any class implementing [`#!java javax.ws.rs.container.ContainerResponseFilter`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/container/ContainerResponseFilter.html) (or extending abstract class implementing it). -Useful for [response modifications](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#jersey-filters). +Any class implementing [`#!java jakarta.ws.rs.container.ContainerResponseFilter`](https://jakarta.ee/specifications/restful-ws/3.0/apidocs/jakarta/ws/rs/container/containerresponsefilter) (or extending abstract class implementing it). +Useful for [response modifications](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#jersey-filters). ```java -@Provider public class MyContainerResponseFilter implements ContainerResponseFilter { @Override @@ -345,11 +342,10 @@ public class MyContainerResponseFilter implements ContainerResponseFilter { ### DynamicFeature -Any class implementing [`#!java javax.ws.rs.container.DynamicFeature`](https://docs.oracle.com/javaee/7/api/javax/ws/rs/container/DynamicFeature.html) (or extending abstract class implementing it). -Useful for conditional [activation of filters](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#jersey-filters). +Any class implementing [`#!java jakarta.ws.rs.container.DynamicFeature`](https://jakarta.ee/specifications/restful-ws/3.0/apidocs/jakarta/ws/rs/container/dynamicfeature) (or extending abstract class implementing it). +Useful for conditional [activation of filters](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#jersey-filters). ```java -@Provider public class MyDynamicFeature implements DynamicFeature { @Override @@ -363,7 +359,6 @@ public class MyDynamicFeature implements DynamicFeature { Any class implementing [`#!java org.glassfish.jersey.server.monitoring.ApplicationEventListener`](https://jersey.java.net/apidocs/2.9/jersey/org/glassfish/jersey/server/monitoring/ApplicationEventListener.html) (or extending abstract class implementing it). ```java -@Provider public class MyApplicationEventListener implements ApplicationEventListener { @Override @@ -376,3 +371,24 @@ public class MyApplicationEventListener implements ApplicationEventListener { } } ``` + +### ModelProcessor + +Any class implementing [`#!java org.glassfish.jersey.server.model.ModelProcessor`](https://eclipse-ee4j.github.io/jersey.github.io/apidocs/2.29.1/jersey/org/glassfish/jersey/server/model/ModelProcessor.html) (or extending abstract class implementing it). + +```java +public class MyModelProcessor implements ModelProcessor { + + @Override + public ResourceModel processResourceModel(ResourceModel resourceModel, + Configuration configuration) { + return resourceModel; + } + + @Override + public ResourceModel processSubResource(ResourceModel subResourceModel, + Configuration configuration) { + return subResourceModel; + } +} +``` diff --git a/src/doc/docs/installers/jersey-feature.md b/dropwizard-guicey/src/doc/docs/installers/jersey-feature.md similarity index 91% rename from src/doc/docs/installers/jersey-feature.md rename to dropwizard-guicey/src/doc/docs/installers/jersey-feature.md index 4e9adac46..d95d9eefe 100644 --- a/src/doc/docs/installers/jersey-feature.md +++ b/dropwizard-guicey/src/doc/docs/installers/jersey-feature.md @@ -5,7 +5,7 @@ ## Recognition -Detects classes implementing `#!java javax.ws.rs.core.Feature` and register their instances in jersey. +Detects classes implementing `#!java jakarta.ws.rs.core.Feature` and register their instances in jersey. It may be useful to configure jersey inside guice components: diff --git a/src/doc/docs/installers/lifecycle.md b/dropwizard-guicey/src/doc/docs/installers/lifecycle.md similarity index 100% rename from src/doc/docs/installers/lifecycle.md rename to dropwizard-guicey/src/doc/docs/installers/lifecycle.md diff --git a/src/doc/docs/installers/listener.md b/dropwizard-guicey/src/doc/docs/installers/listener.md similarity index 80% rename from src/doc/docs/installers/listener.md rename to dropwizard-guicey/src/doc/docs/installers/listener.md index 5c6772f67..b5ceb9cdc 100644 --- a/src/doc/docs/installers/listener.md +++ b/dropwizard-guicey/src/doc/docs/installers/listener.md @@ -7,7 +7,7 @@ Register new web listener in main or admin contexts. ## Recognition -Detects classes annotated with `@javax.servlet.annotation.WebListener` annotation and register them in dropwizard environment. +Detects classes annotated with `@jakarta.servlet.annotation.WebListener` annotation and register them in dropwizard environment. ```java @WebListener @@ -19,13 +19,13 @@ public class MyListener implements ServletContextListener, ServletRequestListene Supported listeners (the same as declared in annotation): - * javax.servlet.ServletContextListener - * javax.servlet.ServletContextAttributeListener - * javax.servlet.ServletRequestListener - * javax.servlet.ServletRequestAttributeListener - * javax.servlet.http.HttpSessionListener - * javax.servlet.http.HttpSessionAttributeListener - * javax.servlet.http.HttpSessionIdListener + * jakarta.servlet.ServletContextListener + * jakarta.servlet.ServletContextAttributeListener + * jakarta.servlet.ServletRequestListener + * jakarta.servlet.ServletRequestAttributeListener + * jakarta.servlet.http.HttpSessionListener + * jakarta.servlet.http.HttpSessionAttributeListener + * jakarta.servlet.http.HttpSessionIdListener !!! warning "" diff --git a/src/doc/docs/installers/managed.md b/dropwizard-guicey/src/doc/docs/installers/managed.md similarity index 95% rename from src/doc/docs/installers/managed.md rename to dropwizard-guicey/src/doc/docs/installers/managed.md index 4dbecd579..896892f4f 100644 --- a/src/doc/docs/installers/managed.md +++ b/dropwizard-guicey/src/doc/docs/installers/managed.md @@ -3,7 +3,7 @@ !!! summary "" CoreInstallersBundle / [ManagedInstaller](https://github.com/xvik/dropwizard-guicey/tree/master/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java) -Installs [dropwizard managed objects](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#managed-objects). +Installs [dropwizard managed objects](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#managed-objects). ## Recognition diff --git a/src/doc/docs/installers/plugin.md b/dropwizard-guicey/src/doc/docs/installers/plugin.md similarity index 99% rename from src/doc/docs/installers/plugin.md rename to dropwizard-guicey/src/doc/docs/installers/plugin.md index 64d97c807..5989e8aeb 100644 --- a/src/doc/docs/installers/plugin.md +++ b/dropwizard-guicey/src/doc/docs/installers/plugin.md @@ -97,7 +97,7 @@ All plugins could be referenced as map: !!! warning As with simple plugin bindings, at least one plugin must be registered so guice could create map binding. - Otherwise, you need to manually declare empty (default) plugnis map binding: + Otherwise, you need to manually declare empty (default) plugins map binding: ```java MapBinder.newMapBinder(binder, keyType, pluginType); - ``` \ No newline at end of file + ``` diff --git a/src/doc/docs/installers/resource.md b/dropwizard-guicey/src/doc/docs/installers/resource.md similarity index 98% rename from src/doc/docs/installers/resource.md rename to dropwizard-guicey/src/doc/docs/installers/resource.md index 82ec1edf1..4c7b0f866 100644 --- a/src/doc/docs/installers/resource.md +++ b/dropwizard-guicey/src/doc/docs/installers/resource.md @@ -3,7 +3,7 @@ !!! summary "" CoreInstallersBundle / [ResourceInstaller](https://github.com/xvik/dropwizard-guicey/tree/master/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java) -Installs [rest resources](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#resources). +Installs [rest resources](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#resources). ## Recognition diff --git a/src/doc/docs/installers/servlet.md b/dropwizard-guicey/src/doc/docs/installers/servlet.md similarity index 96% rename from src/doc/docs/installers/servlet.md rename to dropwizard-guicey/src/doc/docs/installers/servlet.md index 5258b0b1a..4dd39ddfd 100644 --- a/src/doc/docs/installers/servlet.md +++ b/dropwizard-guicey/src/doc/docs/installers/servlet.md @@ -7,7 +7,7 @@ Register new servlet in main or admin contexts. ## Recognition -Detects classes annotated with `@javax.servlet.annotation.WebServlet` annotation and register them in dropwizard environment. +Detects classes annotated with `@jakarta.servlet.annotation.WebServlet` annotation and register them in dropwizard environment. ```java @WebServlet("/mapped") diff --git a/src/doc/docs/installers/task.md b/dropwizard-guicey/src/doc/docs/installers/task.md similarity index 96% rename from src/doc/docs/installers/task.md rename to dropwizard-guicey/src/doc/docs/installers/task.md index f601e2f21..dc547a20e 100644 --- a/src/doc/docs/installers/task.md +++ b/dropwizard-guicey/src/doc/docs/installers/task.md @@ -3,7 +3,7 @@ !!! summary "" CoreInstallersBundle / [TaskInstaller](https://github.com/xvik/dropwizard-guicey/tree/master/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java) -Installs [dropwizard tasks](https://www.dropwizard.io/en/release-2.0.x/manual/core.html#tasks). +Installs [dropwizard tasks](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#tasks). ## Recognition diff --git a/dropwizard-guicey/src/doc/docs/reference/forms.md b/dropwizard-guicey/src/doc/docs/reference/forms.md new file mode 100644 index 000000000..662d7ed3a --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/reference/forms.md @@ -0,0 +1,121 @@ + +## Urlencoded forms + +Single fields: + +```java + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotNull @FormParam("field1") String field1, + @NotNull @FormParam("field2") String field1) { + } +``` + +There might be multiple values with the same name: + +```java + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotNull @FormParam("field") List values) { + } +``` + +All values: + +```java + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotEmpty MultivaluedMap params) { + } +``` + +## GET + +Urlencoded forms might post values with GET using query parameters: + +```java + @Path("/get") + @GET + public String get(@NotNull @QueryParam("field1") String field1, + @NotNull @QueryParam("field1") String field2) { + } +``` + +Multiple values are also possible: + +```java + @Path("/get") + @GET + public String get(@NotEmpty @QueryParam("field") List values) { + } +``` + +## Mutlipart + +Simple parameters: + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post( + @NotNull @FormDataParam("field1") String field1, + @NotNull @FormDataParam("field2") String feild2) { + } +``` + +Simple file upload: + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) { + } +``` + +File (or any simple field): + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post(@NotNull @FormDataParam("file") FormDataBodyPart file) { + } +``` + +Multiple values: + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post(@NotNull @FormDataParam("file") List files) { + } +``` + +Also, content-dispositions might be aggregated: + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post(@NotNull @FormDataParam("file") List files) { + } +``` + +All multipart fields: + +```java + @Path("/post") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String post(@NotNull FormDataMultiPart multiPart) { + Map> fieldsMap = multiPart.getFields(); + } +``` \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/reference/jvm.md b/dropwizard-guicey/src/doc/docs/reference/jvm.md new file mode 100644 index 000000000..c6835ff8d --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/reference/jvm.md @@ -0,0 +1,227 @@ +# Essential JVM Heap Settings + +!!! important + [Original article](https://medium.com/itnext/essential-jvm-heap-settings-what-every-java-developer-should-know-b1e10f70ffd9?sk=24f9f45adabf009d9ccee90101f5519f) (source) + +JVM Heap optimization in newer Java versions is highly advanced and container-ready. +This is great to quickly get an application in production without having to deal with +various JVM heap related flags. But the default JVM heap and GC settings might surprise +you. Know them before your first OOMKilled encounter. + +!!! tip "" + You need to be on Java 9+ for anything written below to be applicable. Still on Java 8? + Time to upgrade Java or job… + +### Running your Java application under the layers of Container or Kubernetes? The environment variable JAVA_TOOL_OPTIONS is your friend + +If you are running in a constrained environment with limited access to modify the comand +`java -jar ...`, don’t worry it is very easy to pass in custom JVM flags. Just set +the environment variable `JAVA_TOOL_OPTIONS` and it will be automatically picked up by +the JDK. This is true for OpenJDK and its variants like RedHat. If you are using a +different JDK, check documentation for an equivalent variable. + +You will see a log line as below during startup: + +``` +Picked up JAVA_TOOL_OPTIONS: -XX:SharedArchiveFile=application.jsa -XX:MaxRAMPercentage=80 +``` + +Be aware that if you have multiple JVM applications running, setting the environment +variable might affect all of them. + +### No idea what heap size or JVM flags are active? Use -XX:+PrintCommandLineFlags + +Unless you have explicitly set the `-Xmx/-Xms` flags, you probably have no idea about +the available heap size. Metrics may give a hint but that is a lagging indicator. +Set the flag `-XX:+PrintCommandLineFlags` to force the JVM to print all active flags +at startup. + +It would look something like this: + +``` +-XX:InitialHeapSize=16777216 -XX:MaxHeapSize=858993459 -XX:MaxRAM=1073741824 -XX:MaxRAMPercentage=80.000000 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SharedArchiveFile=application.jsa -XX:-THPStackMitigation -XX:+UseCompressedOops -XX:+UseSerialGC +``` + +This is useful to gain insights on your current JVM setup. + +Another flag `-XX:+PrintFlagsFinal` shows every flag including defaults. But it might +be overkill to include in every application startup. If your Java application is +wrapped inside a container image, this command is a quick way to see the JVM flags that +will be applied: `docker run --rm --entrypoint java myimage:latest -XX:+PrintFlagsFinal -version` + +### I am applying container memory limits. Do I need to set heap flags? + +It depends. For a typical application not requiring optimizations, the default behaviour +would be fine. JVM will automatically apply a percentage of available memory as maximum +heap size. Just make sure to leave some space for non-heap stuff, sidecars, agents, etc. +How much? Read on. + +### By default only 25% of available memory is used as max heap! + +With many JDK vendors, a container with 1GB memory limit will only get 256 MB of maximum +heap size. This is due to the default flag `-XX:MaxRAMPercentage=25` set. +This conservative number made sense back in the non-container days when multiple JVMs would run +on the same machine. But when running in containers with memory limits set correctly, +this value can be increased to 60, 70 or even 80% depending on the application’s non-heap +memory usage like byte buffers, page cache etc. + +``` +> docker run --memory 2g openjdk:24 java -XX:+PrintFlagsFinal -version | grep MaxRAMPercentage + double MaxRAMPercentage = 25.000000 {product} {default} +``` + +### Garbage Collection algorithm changes depending on available memory + +Since Java 9, G1 is the default garbage collection algorithm replacing Parallel GC in +previous versions. But there is a caveat! This applies only if available memory (not heap +size) is at least 2 GB. Below 2 GB, serial GC is the default algorithm. + +``` +> docker run --memory 1g openjdk:24 java -XX:+PrintFlagsFinal -version | grep -E "UseSerialGC | UseG1GC" + bool UseG1GC = false {product} {default} + bool UseSerialGC = true {product} {ergonomic} + +> docker run --memory 2g openjdk:24 java -XX:+PrintFlagsFinal -version | grep -E "UseSerialGC | UseG1GC" + bool UseG1GC = true {product} {ergonomic} + bool UseSerialGC = false {product} {default} +``` + +This is likely due to the fact that G1 GC carries overhead of metadata and its own bookkeeping +which outweighs the benefits in low-memory applications. + +You can always set your own GC algorithm with flags like `-XX:+UseG1GC` and `-XX:+UseSerialGC`. + +### Kubernetes pods memory limit affect heap sizes + +The memory limit set on the pod affect the heap size calculations. The memory request +has no impact. It only affects the scheduling of the pod on a node. + +### JVM flag UseContainerSupport is not necessary + +Since Java 10+, the JVM flag UseContainerSupport is available and always enabled by default. + +``` +> docker run --memory 1g openjdk:24 java -XX:+PrintFlagsFinal -version | grep UseContainerSupport + bool UseContainerSupport = true {product} {default} +``` + +### Common Heap Regions + +The JVM heap space is broadly divided into two regions or generations — Young and Old +generation. The Young generation is further divided into Eden and Survivor space. +The survivor space consists of two equally divided spaces S0 and S1. + +A newly created object is born in the Eden space. If it survives one or two garbage +collections, it is promoted to the Survivor space. If it survives even more garbage +collections, it is considered an elder and promoted to the Tenured or Old space. + +``` +Total heap size = Eden space + Survivor space + Tenured space +``` + +### Metrics Gotchas for Serial and G1 GC + +Typical Heap monitoring view for Serial GC + +[![Monitoring 1](../img/jvm/jvm1.webp)](https://channel.io "Typical Heap monitoring view for Serial GC") + +When the available memory is less than 2 GB and Serial GC is active, the max sizes of +Eden, Survivor and Tenured spaces will be fixed. The size of Young generation +(Eden + Survivor) is determined by `MaxNewSize` which usually defaults to 1/3rd of the +max heap size. Within the young generation, the sizing of Eden and Survivor is +determined via `NewRatio` and `SurvivorRatio`. These default to 2 and 8 respectively in +OpenJDK. Effectively, old generation will be twice the size of young generation and +the Survivor space is 1/8th the size of Eden space. + +``` +Heap breakup under 2 GB / Serial GC + +Container memory limit = 1 GB +|_ Max heap size = 256 MB (25%) + |_ Young generation =~ 85 MB (1/3 of heap size) + |_ Eden space =~ 76 MB (85 * 8/9) + |_ Survivor space =~ 9 MB (85 * 1/9, S0 = 4.5 MB, S1 = 4.5 MB) + |_ Old generation =~ 171 MB (max heap size - young generation) +``` + +These numbers would approximately reflect in the JVM heap metrics. + +Typical Heap monitoring view for G1 GC + +[![Monitoring 2](../img/jvm/jvm2.webp)](https://channel.io "Typical Heap monitoring view for G1 GC") + +The most striking difference in metrics for G1 GC compared to Serial GC is that the +max sizes of Eden and Survivor spaces show as zero. This is because in G1 GC, the size +of these spaces are not fixed and is resized after every GC cycle. This can be +confusing in the metrics as the values are non-zero while max is zero. The flags +`MaxNewSize`, `NewRatio` and `SurvivorRatio` apply to generational GCs like Serial and +Parallel only and not G1. + +``` +Heap breakup over 2 GB / G1 GC + +Container memory limit = 2GB +|_ Max heap size = 512 MB (25%) + |_ Young generation =~ Adaptive + |_ Eden space =~ Adaptive / -1 as reported by metrics + |_ Survivor space =~ Adaptive / -1 as reported by metrics + |_ Old generation =~ Adaptive / 512 MB as reported by metrics +``` + +### Metaspace and Compressed Class Space + +Misleading Metaspace and Compressed Class Space metric + +[![Monitoring 3](../img/jvm/jvm3.webp)](https://channel.io "Misleading Metaspace and Compressed Class Space metric") + +Outside of heap, an important memory region is the Metaspace which stores information +about loaded classes, methods, fields, annotations, constants, and JIT code. The size +of Metaspace is determined by the flag `MaxMetaspaceSize` which is by default unlimited. +It can use all native memory outside of heap and within the available memory. If +usage goes beyond this, you would see `java.lang.OutOfMemoryError: Metaspace`. Large +number of loaded classes will increase the Metaspace usage. + +Compressed Class Space stores ordinary object pointers ([oops](https://wiki.openjdk.org/display/HotSpot/CompressedOops)) to Java objects by +compressing them from 64 to 32-bit offsets thereby saving some valuable memory space. +More importantly, it is a sub-region of the Metaspace. The metrics report the size of +Compressed Class space as 1 GB since the flag `CompressedClassSpaceSize` is set to 1 GB +by default irrespective of available memory. It is not allocated unless needed. But +since this is a sub-region of Metaspace, setting `MaxMetaspaceSize` is enough. + +### Reserved Code Cache + +Different regions of the JVM’s code cache + +[![Monitoring 4](../img/jvm/jvm4.webp)](https://channel.io "Different regions of the JVM’s code cache") + +This is the memory space outside heap that stores the native code generated by +Just-In-Time (JIT) compiler. + +Java source code is compiled into Java binary code which is executed by the JVM. +JVM interprets the binary code into OS-specific machine code line-by-line upon every +execution. While this is enough, it would be very slow. The JIT compiler identifies +hotspots (code paths that are frequently accessed), compiles them into native code +and stores it in the Reserved Code Cache. The next time the hot code path requires +execution, no interpretation is needed as the corresponding native code is directly +invoked. + +> Interpreter is like asking a professional translator to translate a phrase in an +> unknown language into a familiar language every time without learning. +> +> JIT compilation is like learning the frequently used phrases in the unknown language +> to not rely on the translator all the time. +> +> AOT compilation is like learning the complete language beforehand and never needing +> the translator. + +By default, the code cache is segmented into multiple regions for optimization. +These regions include `non-nmethods` (unrelated to user code, internal to JIT compiler), +`non-profiled nmethods` (native methods that have not been profiled yet) and `profiled +nmethods` (native methods that have been aggressively optimized). The total size of +reserved code cache is defined via the flag `ReservedCodeCacheSize` and defaults to +240 MB since Java 10. + +### Conclusion +While there is much more to study in this area, I consider the things listed here +as must-know for every Java developer. The next time you encounter OOM errors, +you can check the JVM metrics and be able to immediately gather relevant information. \ No newline at end of file diff --git a/dropwizard-guicey/src/doc/docs/reference/matrix.md b/dropwizard-guicey/src/doc/docs/reference/matrix.md new file mode 100644 index 000000000..5dbfe7056 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/reference/matrix.md @@ -0,0 +1,22 @@ +# Matrix params + +Matrix param are almost the same as query params: + +/some;m=1?q=2 + +```java + @Path("/some") + public Response get(@MatrixParam("m") String m, @QueryParam("q") String q) +``` + +But matrix params could appear in the middle of path: + +/some;m=1/path?q=2 + +```java + @Path("{vars:some}/path") + public Response get(@PathParam("vars") PathSegment vars, @QueryParam("1") String q) +``` + +`@MatrixParam` annotation can't be used here, instead using path param with static regex ("some"). +Actual matrix parameters are available from `PathSegment.getMatrixParams()` diff --git a/dropwizard-guicey/src/doc/docs/reference/rest-client.md b/dropwizard-guicey/src/doc/docs/reference/rest-client.md new file mode 100644 index 000000000..0b444adc5 --- /dev/null +++ b/dropwizard-guicey/src/doc/docs/reference/rest-client.md @@ -0,0 +1,345 @@ +# Rest client + +Rest client creation: + +```java + public void test (ClientSupport client) { + // resource client + ResourceClient rest = client.restClient(RestResource.class); + } +``` + +or from root rest client: + +```java + ResourceClient rest = client.restClient().subClient(RestResource.class); +``` + +## Method call examples + +```java + @GET + @Path("/get/{name}") + public List get(@PathParam("name") String param) +``` + +```java + // path param applied + assertThat(rest.method(r -> r.get("path")).as(String.class)) + .isEqualTo("something") +``` + +```java + @Path("/entity") + @POST + public String entity(ModelType model) +``` + +```java + // post with entity + assertThat(rest.method(r -> r.entity(new MediaType(...))).as(String.class)) + .isEqualTo("something") +``` + +### Response types + +```java + // execute request, no success status check + TestClientResponse res = rest.method(RestResource::get).invoke() + // throw exception if not success and AssertionError if not provided status + TestClientResponse res = rest.method(RestResource::get).expectSuccess() + // throw AssertionError if success or not provided status + TestClientResponse res = rest.method(RestResource::get).expectFailure() + // throw AssertionError if not redirect or not provided status + TestClientResponse res = rest.method(RestResource::get).expectRedirect() + // execute, ignore response, throw error if not successful + rest.method(RestResource::get).asVoid() + // execute with successful result conversion, exception otherwise + String res = rest.method(RestResource::get).asString() + // execute with successful result conversion, exception otherwise + MyType res = rest.method(RestResource::get).as(MyType.class) + // execute with successful result conversion, exception otherwise + List res = rest.method(RestResource::get).as(new GenericType<>{}) +``` + +### Response assertions + +```java + rest.method(RestResource::get).invoke() + .assertSuccess() // assertFaile() assertRedirect() + .assertStatus(200) + .assertVoidResponse() + .assertMedia(MediaType.APPLICATION_JSON_TYPE) + .assertLocale(Locale.EN) + .assertheader("Name", "value") + .assertCookie("Cookie", "value") + .assertCacheControl(cc -> cc.isMustRevalidate()) +``` + +```java + MyType res = rest.method(RestResource::get).invoke() + .assertLocale(Locale.EN) + .assertheader("Name", "value") + .as(MyType.class) +``` + +### Request assertions + +```java +rest.defaultHeader("Token", "abc") + +MyType res = rest.method(RestResource::get) + .assertRequest(tracker -> assertThat(tracker.getHeaders().get("Token")).isEqualsTo("abc")) + .as(MyType.class) +``` + + +## Sub resources + +### Sub resource by instance + +```java + @Path("/sub") + public SubResource sub() { + return new SubResource(); + } +``` + +```java + // sub resource called + assertThat(rest.method(r -> r.sub().get("path")).as(String.class)) + .isEqualTo("something") +``` + +### Sub resource by class + +```java + @Path("/sub") + public Class sub() { + return SubResource.class; + } +``` + +```java + // manual sub client creation (path resolved from locator method) + assertThat(rest.subResourceClient(Resource::sub, SubResource.class) + .method(SubResource::get).asString()) + .isEqualTo("something"); +``` + +## File download + +```java + @GET + @Path("/download") + public Response download() { + return Response.ok(getClass().getResourceAsStream("/some.txt")) + .header(HttpHeader.CONTENT_DISPOSITION.toString(), "attachment; filename=some.txt") + .build(); + } +``` + +```java + // load in directory, preserving file name + File res = rest.method(FileResource::download).expectSuccess().asFile(temp); +``` + +## Exact status check + +```java + // fail for different status + rest.method(r -> r.post(entity)).expectSuccess(201) + // example response assertion + .assertHeader("Some", "11"); +``` + + +## Error check + +```java + assertThatThrownBy(() -> rest.method(RestResource::get).expectFailure(401)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Unexpected response status 500 when expected 401"); +``` + +## Redirects + +```java + @Path("/redirect") + @GET + public Response redirect() { + return Response.seeOther( + urlBuilder.rest(getClass()).method(RestResource::get).buildUri() + ).build(); + } +``` + +```java + rest.method(RestResource::redirect).expectRedirect() // optional status + .assertHeader("Location", s -> s.endsWith("/get")); +``` + +## Urlencoded forms + +### Simple values + +```java + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotNull @FormParam("name") String value, + @NotNull @FormParam("date") String date) +``` + +```java + // entity from parameters + assertThat(rest.method(r -> r.post("1", "2")).as(String.class)) + .isEqualTo("something"); + + // manual call + assertThat(rest.buildForm("/post") + .param("name", "1") + .param("date", "2") + .buildPost() + .as(String.class)) + .isEqualTo("something"); + + // manual entity building + assertThat(rest.method(r -> r.post(null, null), + rest.buildForm(null) + .param("name", "1") + .param("date", "2") + .buildEntity()) + .as(String.class)) + .isEqualTo("something"); +``` + +### Multiple values + +```java + @Path("/postMulti") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String postMulti(@NotEmpty @FormParam("name") List value, + @NotNull @FormParam("date") String date) +``` + +```java + assertThat(rest.buildForm("/postMulti") + .param("name", 1, 2, 3) + .param("date", "2") + .buildPost() + .as(String.class)).isEqualTo("something"); + + assertThat(rest.method(instance -> instance.postMulti(Arrays.asList("1", "2", "3"), "2")) + .as(String.class)).isEqualTo("something"); +``` + +### Not mapped fields + +```java + @Path("/post2") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post2(@NotEmpty MultivaluedMap params) +``` + +```java + final MultivaluedHashMap map = new MultivaluedHashMap<>(); + map.add("name", "1"); + map.add("date", "2"); + assertThat(rest.method(r -> r.post2(map)).as(String.class)) + .isEqualTo("somthing"); +``` + +## Multipart forms + +### File upload + +```java + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) +``` + +```java + // manual + assertThat(rest.buildForm("/multipart") + .param("file", new File("src/test/resources/some.txt")) + .buildPost() + .as(String.class)).isEqualTo("something"); + + // from method call + assertThat(rest.multipartMethod((r, multipart) -> + r.multipart(multipart.fromClasspath("/logback.xml"), + multipart.disposition("file", "logback.xml"))) + .as(String.class)).isEqualTo("something"); +``` + +### File upload 2 + +```java + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2( + @NotNull @FormDataParam("file") FormDataBodyPart file) +``` + +```java + assertThat(rest.multipartMethod((r, multipart) -> + // from classpath + r.multipart2(multipart.streamPart("file", "/some.txt"))) + .as(String.class)).isEqualTo("something"); + + assertThat(rest.multipartMethod((r, multipart) -> + // from fs (relative to work dir) + r.multipart2(multipart.filePart("file", "src/test/resources/some.txt"))) + .as(String.class)).isEqualTo("something"); +``` + +### Multiple files within one field + +```java + @Path("/multipartMulti") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartMulti( + @NotNull @FormDataParam("file") List file) +``` + +```java + // from method call + assertThat(rest.multipartMethod((r, multipart) -> + r.multipartMulti(Arrays.asList( + multipart.filePart("file", "src/test/resources/some1.txt"), + multipart.filePart("file", "src/test/resources/some2.txt")))) + .as(String.class)).isEqualTo("something"); + + // manual + assertThat(rest.buildForm("/multipartMulti") + .param("file", new File("src/test/resources/some1.txt"), new File("src/test/resources/some2.txt")) + .buildPost() + .as(String.class)).isEqualTo("something"); +``` + +### Generic multipart + +```java + @Path("/multipartGeneric") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartGeneric(@NotNull FormDataMultiPart multiPart) +``` + +```java + assertThat(rest.multipartMethod((r, multipart) -> + r.multipartGeneric(multipart.multipart() + .field("foo", "bar") + .stream("file", "/some.txt") + .build())) + .as(String.class)).isEqualTo("something"); +``` \ No newline at end of file diff --git a/src/doc/docs/tests.md b/dropwizard-guicey/src/doc/docs/tests.md similarity index 77% rename from src/doc/docs/tests.md rename to dropwizard-guicey/src/doc/docs/tests.md index 11749f132..c0bbb8383 100644 --- a/src/doc/docs/tests.md +++ b/dropwizard-guicey/src/doc/docs/tests.md @@ -1,6 +1,13 @@ # Testing -You can use all existing [dropwizard testing tools](https://www.dropwizard.io/en/stable/manual/testing.html) for unit tests. +Core [dropwziard testing support](https://www.dropwizard.io/en/stable/manual/testing.html) +proposes atomic testing approach (separate testing of each element, which you still could use when possible). + +With DI (guice) we have to move towards **integration testing** because: + +1. It is now harder to mock classes "manually" (because of DI "black box") +2. We have a core (guice injector, without web services), starting much faster than + complete application. ## Guicey tests @@ -108,7 +115,7 @@ Or just override exact values (without declaring config file): In many cases, you don't need the entire application, but just a working `Injector` to check core application logic. -For such cases, guicey provides lightweight extensions like [@TestGuiceyApp](guide/test/junit5.md#testguiceyapp): +For such cases, guicey provides lightweight extensions like [@TestGuiceyApp](guide/test/junit5/run.md#testing-core-logic): - will not start jetty (no ports bind, no HK2 launched) - start `Managed` objects to simulate lifecycle @@ -130,7 +137,11 @@ public class MyTest { ... } } -``` +``` + +!!! tip + There is also a special [lightweight REST](guide/test/junit5/rest.md) tests support to + avoid starting entire web server. ## Spock @@ -166,4 +177,31 @@ class MyTest extends Specification { } ``` -See [Spock 2 docs](guide/test/spock2.md) for more details. \ No newline at end of file +See [Spock 2 docs](guide/test/spock2.md) for more details. + +## Testing commands + +Guicey also provides special support for [testing dropwizard commands](guide/test/general/command.md): + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .run("simple", "-u", "user") + +Assertions.assertTrue(result.isSuccessful()); +``` + +* Such run never fails (in case of error exception would be inside result object) +* Result countain all required objects for assertions and contains +* Full console output is accessible for assertions +* Could mock user input (for commands requiring interaction) + +Also commands could be used to check application failures on startup (self-checks testing): + +```java +CommandResult result = TestSupport.buildCommandRunner(App.class) + .runApp() +``` + +Such test would fail in case of successful application start. +No additional mocks or extensions required because running like this would not cause +`System.exist(1)` call, performed in `Application` class (see `Application.onFatalError`). \ No newline at end of file diff --git a/src/doc/mkdocs.yml b/dropwizard-guicey/src/doc/mkdocs.yml similarity index 62% rename from src/doc/mkdocs.yml rename to dropwizard-guicey/src/doc/mkdocs.yml index 931290fd1..e16419a71 100644 --- a/src/doc/mkdocs.yml +++ b/dropwizard-guicey/src/doc/mkdocs.yml @@ -2,14 +2,14 @@ site_name: Dropwizard-guicey site_description: 'Dropwizard guice integration' site_author: 'Vyacheslav Rusakov' site_url: 'https://xvik.github.io/dropwizard-guicey' -edit_uri: edit/master/src/doc/docs/ +edit_uri: edit/master/dropwizard-guicey/src/doc/docs/ # Repository repo_name: 'dropwizard-guicey' repo_url: 'https://github.com/xvik/dropwizard-guicey' # Copyright -copyright: 'Copyright © 2014-2022 Vyacheslav Rusakov' +copyright: 'Copyright © 2014-2025 Vyacheslav Rusakov' plugins: - search @@ -46,8 +46,8 @@ extra: social: - icon: fontawesome/brands/github link: https://github.com/xvik - - icon: fontawesome/brands/twitter - link: https://twitter.com/vyarus +# - icon: fontawesome/brands/twitter +# link: https://twitter.com/vyarus # Google Analytics analytics: @@ -74,8 +74,8 @@ markdown_extensions: - pymdownx.caret - pymdownx.details - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight - pymdownx.inlinehilite - pymdownx.keys @@ -116,22 +116,64 @@ nav: - ServletModule: guide/guice/servletmodule.md - Diagnostic: - Tools: guide/diagnostic/diagnostic-tools.md + - Startup times: guide/diagnostic/startup-report.md + - Extensions help: guide/diagnostic/extensions-report.md - Configuration: guide/diagnostic/configuration-report.md - Configuration model: guide/diagnostic/configuration-model.md - Installers: guide/diagnostic/installers-report.md - Yaml values: guide/diagnostic/yaml-values-report.md - Lifecycle: guide/diagnostic/lifecycle-report.md - Guice: guide/diagnostic/guice-report.md + - Guice provision time: guide/diagnostic/guice-provision-report.md - AOP: guide/diagnostic/aop-report.md - Web: guide/diagnostic/web-report.md - Jersey: guide/diagnostic/jersey-report.md + - Shared state: guide/diagnostic/shared-state-report.md - Test: - - Overview: guide/test/overview.md + - Base concepts: guide/test/overview.md + - AssertJ: guide/test/assertj.md + - General tools: + - Overview: guide/test/general/general.md + - Testing application: guide/test/general/run.md + - Testing REST: guide/test/general/rest.md + - Testing web (HTTP client): guide/test/general/client.md + - Testing commands: guide/test/general/command.md + - Testing startup fails: guide/test/general/startup.md + - Testing console output: guide/test/general/output.md + - Testing logs: guide/test/general/logs.md + - Testing with stubs: guide/test/general/stubs.md + - Testing with mocks: guide/test/general/mocks.md + - Testing with spies: guide/test/general/spies.md + - Testing performance (bean tracking): guide/test/general/tracks.md + - Junit 5: + - Setup: guide/test/junit5/setup.md + - Testing application: guide/test/junit5/run.md + - Application modification: guide/test/junit5/hooks.md + - Application configuration: guide/test/junit5/config.md + - Guice injections: guide/test/junit5/inject.md + - Test environment setup: guide/test/junit5/setup-object.md + - Testing REST: guide/test/junit5/rest.md + - Testing web (HTTP client): guide/test/junit5/client.md + - Testing commands: guide/test/junit5/command.md + - Testing startup fails: guide/test/junit5/startup.md + - Testing console output: guide/test/junit5/output.md + - Testing logs: guide/test/junit5/logs.md + - Testing with stubs: guide/test/junit5/stubs.md + - Testing with mocks: guide/test/junit5/mocks.md + - Testing with spies: guide/test/junit5/spies.md + - Testing performance (bean tracking): guide/test/junit5/tracks.md + - Tests unification: guide/test/junit5/unification.md + - Debug: guide/test/junit5/debug.md + - Nested tests: guide/test/junit5/nested.md + - Environment variables: guide/test/junit5/env.md + - Parallel execution: guide/test/junit5/parallel.md + - Junit extensions integration: guide/test/junit5/junit-ext.md + - Testing extensions: guide/test/junit5/test-ext.md - Spock 2: guide/test/spock2.md - - Junit 5: guide/test/junit5.md - - General support: guide/test/general.md - Spock 1: guide/test/spock.md - Junit 4: guide/test/junit4.md + - OpenAPI fake server: guide/test/openapi-server.md + - App URL builder: guide/url-builder.md - Lifecycle: guide/lifecycle.md - Classpath scan: guide/scan.md - Installers: guide/installers.md @@ -162,7 +204,6 @@ nav: - Admin REST: extras/admin-rest.md - Lifecycle annotations: extras/lifecycle-annotations.md - Guava EventBus: extras/eventbus.md - - JDBI: extras/jdbi.md - JDBI3: extras/jdbi3.md - SPA: extras/spa.md - Server Pages: extras/gsp.md @@ -172,11 +213,11 @@ nav: - Governator: examples/governator.md - Hibernate: examples/hibernate.md - EventBus: examples/eventbus.md - - JDBI: examples/jdbi.md - JDBI3: examples/jdbi3.md - About: - Release notes: about/release-notes.md - Compatibility: about/compatibility.md + - Migration guide: about/migration.md - Version history: about/history.md - Support: about/support.md - License: about/license.md \ No newline at end of file diff --git a/src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java similarity index 70% rename from src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java index 3432564e1..c9f11e0a0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceBundle.java @@ -5,15 +5,31 @@ import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; -import io.dropwizard.Configuration; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.lifecycle.ServerLifecycleListener; +import jakarta.servlet.DispatcherType; +import org.eclipse.jetty.util.component.LifeCycle; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; import ru.vyarus.dropwizard.guice.bundle.DefaultBundleLookup; import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup; import ru.vyarus.dropwizard.guice.bundle.lookup.VoidBundleLookup; -import ru.vyarus.dropwizard.guice.debug.*; +import ru.vyarus.dropwizard.guice.debug.ConfigurationDiagnostic; +import ru.vyarus.dropwizard.guice.debug.ExtensionsHelpDiagnostic; +import ru.vyarus.dropwizard.guice.debug.GuiceAopDiagnostic; +import ru.vyarus.dropwizard.guice.debug.GuiceBindingsDiagnostic; +import ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic; +import ru.vyarus.dropwizard.guice.debug.JerseyConfigDiagnostic; +import ru.vyarus.dropwizard.guice.debug.LifecycleDiagnostic; +import ru.vyarus.dropwizard.guice.debug.SharedStateDiagnostic; +import ru.vyarus.dropwizard.guice.debug.StartupTimeDiagnostic; +import ru.vyarus.dropwizard.guice.debug.WebMappingsDiagnostic; +import ru.vyarus.dropwizard.guice.debug.YamlBindingsDiagnostic; import ru.vyarus.dropwizard.guice.debug.hook.DiagnosticHook; +import ru.vyarus.dropwizard.guice.debug.hook.GuiceProvisionTimeHook; +import ru.vyarus.dropwizard.guice.debug.hook.StartupTimeHook; import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig; import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceAopConfig; import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig; @@ -30,6 +46,7 @@ import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; import ru.vyarus.dropwizard.guice.module.context.option.Option; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; import ru.vyarus.dropwizard.guice.module.context.unique.DuplicateConfigDetector; import ru.vyarus.dropwizard.guice.module.context.unique.UniqueItemsDuplicatesDetector; import ru.vyarus.dropwizard.guice.module.installer.CoreInstallersBundle; @@ -38,18 +55,27 @@ import ru.vyarus.dropwizard.guice.module.installer.WebInstallersBundle; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationShutdownListener; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationShutdownListenerAdapter; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationStartupListener; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationStartupListenerAdapter; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.GuiceyStartupListener; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.GuiceyStartupListenerAdapter; import ru.vyarus.dropwizard.guice.module.installer.internal.CommandSupport; import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; -import ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule; -import javax.servlet.DispatcherType; import java.util.EnumSet; import java.util.Map; import java.util.function.Consumer; import java.util.function.Predicate; -import static ru.vyarus.dropwizard.guice.GuiceyOptions.*; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.GuiceFilterRegistration; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.InjectorStage; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.ScanPackages; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.SearchCommands; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.UseCoreInstallers; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.UseHkBridge; import static ru.vyarus.dropwizard.guice.module.installer.InstallersOptions.JerseyExtensionsManagedByGuice; /** @@ -98,8 +124,8 @@ * @see ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo for configuratio diagnostic * @since 31.08.2014 */ -@SuppressWarnings({ - "PMD.ExcessiveClassLength", "PMD.ExcessiveImports", "PMD.TooManyMethods", "PMD.ExcessivePublicCount"}) +@SuppressWarnings({"PMD.ExcessiveImports", "PMD.TooManyMethods", + "PMD.ExcessivePublicCount", "PMD.GodClass", "PMD.CouplingBetweenObjects"}) public final class GuiceBundle implements ConfiguredBundle { private final ConfigurationContext context = new ConfigurationContext(); @@ -108,6 +134,9 @@ public final class GuiceBundle implements ConfiguredBundle { GuiceBundle() { // Bundle should be instantiated only from builder + context.stat().timer(Stat.GuiceyTime); + context.stat().timer(Stat.ConfigurationTime); + context.stat().timer(Stat.BundleBuilderTime); } @Override @@ -121,8 +150,8 @@ public void initialize(final Bootstrap bootstrap) { starter.findCommands(); // scan for installers (if scan enabled) and installers initialization starter.resolveInstallers(); - // scan for extensions (if scan enabled) and validation of all registered extensions - starter.resolveExtensions(); + // scan for extensions, if scan enabled. (no installation because more extensions could be added in run phase) + starter.scanExtensions(); starter.initFinished(); } @@ -134,6 +163,8 @@ public void run(final Configuration configuration, final Environment environment // process guicey bundles runner.runBundles(); + // register all manual and classpath scan extensions (bundles may register more extensions) + runner.registerExtensions(); // prepare guice modules for injector creation runner.prepareModules(); // create injector @@ -141,6 +172,8 @@ public void run(final Configuration configuration, final Environment environment runner.analyzeAndRepackageBindings()); // install extensions by instance runner.installExtensions(); + // injected services could be used in run application method + runner.injectApplication(); // inject command fields runner.injectCommands(); @@ -149,7 +182,7 @@ public void run(final Configuration configuration, final Environment environment /** * Note that injector could be accessed statically anywhere with - * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup#getInjector(io.dropwizard.Application)}. + * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup#getInjector(io.dropwizard.core.Application)}. * * @return created injector instance or fail if injector not yet created * @throws IllegalStateException if injector is not yet created @@ -164,8 +197,12 @@ public Injector getInjector() { */ public static Builder builder() { return new Builder() - // allow enabling diagnostic logs with system property (on compiled app): -Dguicey.hooks=diagnostic - .hookAlias("diagnostic", DiagnosticHook.class); + // enable diagnostic logs with system property (on compiled app): -Dguicey.hooks=diagnostic + .hookAlias(DiagnosticHook.ALIAS, DiagnosticHook.class) + // enable startup logs with: -Dguicey.hooks=startup-time + .hookAlias(StartupTimeHook.ALIAS, StartupTimeHook.class) + // enable guice provision time logs with: -Dguicey.hooks=provision-time + .hookAlias(GuiceProvisionTimeHook.ALIAS, GuiceProvisionTimeHook.class); } /** @@ -174,36 +211,7 @@ public static Builder builder() { @SuppressWarnings({"checkstyle:ClassDataAbstractionCoupling", "checkstyle:ClassFanOutComplexity"}) public static class Builder { private final GuiceBundle bundle = new GuiceBundle(); - - /** - * Guicey broadcast a lot of events in order to indicate lifecycle phases - * ({@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle}). This could be useful - * for diagnostic logging (like {@link #printLifecyclePhases()}) or to implement special - * behaviours on installers, bundles, modules extensions (listeners have access to everything). - * For example, {@link ConfigurationAwareModule} like support for guice modules could be implemented - * with listeners. - *

- * Configuration items (modules, extensions, bundles) are not aware of each other and listeners - * could be used to tie them. For example, to tell bundle if some other bundles registered (limited - * applicability, but just for example). - *

- * You can also use {@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter} when you need to - * handle multiple events (it replaces direct events handling with simple methods). - *

- * Listener is not registered if equal listener were already registered ({@link java.util.Set} used as - * listeners storage), so if you need to be sure that only one instance of some listener will be used - * implement {@link Object#equals(Object)} and {@link Object#hashCode()}. - * - * @param listeners guicey lifecycle listeners - * @return builder instance for chained calls - * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle - * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter - * @see ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener - */ - public Builder listen(final GuiceyLifecycleListener... listeners) { - bundle.context.lifecycle().register(listeners); - return this; - } + private boolean buildCalled; /** * Options is a generic mechanism to provide internal configuration values for guicey and 3rd party bundles. @@ -238,7 +246,7 @@ public Builder listen(final GuiceyLifecycleListener... listeners) { * @see GuiceyOptions * @see ru.vyarus.dropwizard.guice.module.installer.InstallersOptions */ - public Builder option(final K option, final Object value) { + public & Option> Builder option(final K option, final Object value) { bundle.context.setOption(option, value); return this; } @@ -318,16 +326,52 @@ public Builder disableBundleLookup() { } /** - * Enables auto scan feature. - * When enabled, all core installers are registered automatically. + * Enables auto scan feature: search for installers and extensions in provided packages. + *

+ * When no packages specified, scan would be performed for application class package. * * @param basePackages packages to scan extensions in * @return builder instance for chained calls * @see GuiceyOptions#ScanPackages */ public Builder enableAutoConfig(final String... basePackages) { - Preconditions.checkState(basePackages.length > 0, "Specify at least one package to scan"); - return option(ScanPackages, basePackages); + final String[] packs = basePackages.length > 0 ? basePackages : new String[]{GuiceyInitializer.APP_PKG}; + return option(ScanPackages, packs); + } + + /** + * Filter classes which could be recognized as an extension. Filter applied before extension recognition, + * so you could slightly increase startup time by avoiding extension detection for some classes. + * Filter also applied for guice bindings (extensions recognized from guice bindings). + *

+ * For example, if you want to exclude all annotated classes: + * {@code autoConfigFilter(type -> !type.isAnnotationPresent(Stub.class))} + *

+ * For simplicity, use {@link ru.vyarus.dropwizard.guice.test.util.ClassFilters} utility with static import + * for building predicates: + * {@code autoConfigFilter(ignoreAnnotated(Stub.class))} + *

+ * Another example, suppose you want spring-style configuration: accept only classes with some annotation: + * {@code autoConfigFilter(annotated(Component.class))} + *

+ * The difference with {@link #disable(java.util.function.Predicate[])} predicate: by default, guicey would + * apply all installers for each class to detect extension and disable predicate prevents recognized extension + * installation, whereas this filter prevents classes from being detected. As a result, even if filtered class + * was an actual extension - it would not be recognized and not registered (not visible in reports). + *

+ * Multiple filters could be specified. + * Filter applied only for extensions (does not affect commands and installers search). + *

+ * Be aware that filter would be also called for all guice bindings (if extension from binding recognition + * enabled). So filter could be used to filter recognitions from bindings too. + * + * @param filter filter predicate for classes suitable for extensions detection + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.test.util.ClassFilters for simple annotation predicates implementation + */ + public Builder autoConfigFilter(final Predicate> filter) { + bundle.context.addAutoScanFilter(filter); + return this; } /** @@ -386,7 +430,10 @@ public Builder uniqueItems(final Class... configurationItems) { * These modules are registered under initialization phase where you don't have access for configuration * or environment objects. To workaround this you can use *AwareModule interfaces, or extend from * {@link ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule} and required objects will be set - * just before configuration start. Another option is to register module inside + * just before configuration start. + *

+ * Alternatively, registration could be delayed with + * {@link #whenConfigurationReady(java.util.function.Consumer)}.And another option is to register module inside * {@link GuiceyBundle#run(GuiceyEnvironment)}, which is called under run phase. * * @param modules one or more guice modules @@ -433,8 +480,9 @@ public Builder modulesOverride(final Module... modules) { * with {@link #enableAutoConfig(String...)}). *

* Enables commands classpath search. All found commands are instantiated and registered in - * bootstrap. Default constructor is used for simple commands, but {@link io.dropwizard.cli.EnvironmentCommand} - * must have constructor with {@link io.dropwizard.Application} argument. + * bootstrap. Default constructor is used for simple commands, but + * {@link io.dropwizard.core.cli.EnvironmentCommand} must have constructor with + * {@link io.dropwizard.core.Application} argument. *

* By default, commands search is disabled. * @@ -466,12 +514,13 @@ public Builder noDefaultInstallers() { * objects provider injections still may be used in resources (will work through HK2 provider). *

* Guice servlets initialization took ~50ms, so injector creation will be a bit faster after disabling. + *

+ * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed. + * In case of hk2 remove, guice request scope will be mandatory. * * @return builder instance for chained calls * @see GuiceyOptions#GuiceFilterRegistration - * @deprecated in the next version HK2 support will be removed and guice request scope will be mandatory */ - @Deprecated public Builder noGuiceFilter() { return option(GuiceFilterRegistration, EnumSet.noneOf(DispatcherType.class)); } @@ -502,6 +551,10 @@ public final Builder installers(final Class... insta *

* Alternatively, you can manually bind extensions in guice module and they would be recognized * ({@link GuiceyOptions#AnalyzeGuiceModules}). + *

+ * If extension registration depends on configuration value then use + * {@link #whenConfigurationReady(java.util.function.Consumer)} or register extension inside + * {@link GuiceyBundle#run(GuiceyEnvironment)}, which is called under run phase. * * @param extensionClasses extension bean classes to register * @return builder instance for chained calls @@ -588,9 +641,13 @@ public final Builder disableInstallers(final Class.. } /** - * Extensions disable is mostly useful for testing. In some cases, it could be used to disable some extra - * extensions installed with classpath scan or bundle. But, generally, try to avoid manual extensions - * disabling for clearer application configuration. + * Extensions disable mostly useful for testing. In some cases, it could be used to disable some extra + * extensions installed with classpath scan or bundle. + *

+ * Disabling could be used for removing some temporal extensions like rest api stubs. + *

+ * For extensions, detected from guice bindings, disabling extension would remove existing guice binding + * (when module analysis enabled). * * @param extensions extensions to disable (manually added, registered by bundles or with classpath scan) * @return builder instance for chained calls @@ -662,7 +719,8 @@ public final Builder disableDropwizardBundles(final Class - * Mostly useful for testing, but in some cases could be used directly. + * Mostly useful for testing, but in some cases could be used directly (e.g. could be used for sisabling + * temporary extensions, like rest api stubs). *

* Use {@link Predicate#and(Predicate)}, {@link Predicate#or(Predicate)} and {@link Predicate#negate()} * to combine complex predicates from simple ones from @@ -700,11 +758,41 @@ public final Builder disableDropwizardBundles(final Class... predicates) { + public final Builder disable(final Predicate... predicates) { bundle.context.registerDisablePredicates(predicates); return this; } + /** + * Guicey broadcast a lot of events in order to indicate lifecycle phases + * ({@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle}). This could be useful + * for diagnostic logging (like {@link #printLifecyclePhases()}) or to implement special + * behaviours on installers, bundles, modules extensions (listeners have access to everything). + * For example, {@link ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule} like support for + * guice modules could be implemented with listeners. + *

+ * Configuration items (modules, extensions, bundles) are not aware of each other and listeners + * could be used to tie them. For example, to tell bundle if some other bundles registered (limited + * applicability, but just for example). + *

+ * You can also use {@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter} when you need to + * handle multiple events (it replaces direct events handling with simple methods). + *

+ * Listener is not registered if equal listener were already registered ({@link java.util.Set} used as + * listeners storage), so if you need to be sure that only one instance of some listener will be used + * implement {@link Object#equals(Object)} and {@link Object#hashCode()}. + * + * @param listeners guicey lifecycle listeners + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle + * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter + * @see ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener + */ + public Builder listen(final GuiceyLifecycleListener... listeners) { + bundle.context.lifecycle().register(listeners); + return this; + } + /** * Enables strict control of beans instantiation context: all beans must be instantiated by guice, except * beans annotated with {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged}. @@ -717,12 +805,12 @@ public final Builder disable(final Predicate... predicates) { *

* To implicitly enable this check in all tests use * {@code PropertyBundleLookup.enableBundles(HK2DebugBundle.class)}. + *

+ * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @return builder instance for chained calls * @see HK2DebugBundle - * @deprecated in the next version HK2 support will be removed and option will become useless */ - @Deprecated public Builder strictScopeControl() { bundle.context.registerBundles(new HK2DebugBundle()); return this; @@ -739,14 +827,14 @@ public Builder strictScopeControl() { * dependency is not available. *

* WARNING: you will not be able to use guice AOP on beans managed by HK2! + *

+ * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @return builder instance for chained calls * @see InstallersOptions#JerseyExtensionsManagedByGuice * @see ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged * @see ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged - * @deprecated in the next version HK2 support will be removed */ - @Deprecated public Builder useHK2ForJerseyExtensions() { option(JerseyExtensionsManagedByGuice, false); option(UseHkBridge, true); @@ -768,11 +856,25 @@ public Builder useHK2ForJerseyExtensions() { * * @return builder instance for chained calls * @see ConfigurationDiagnostic + * @see ru.vyarus.dropwizard.guice.debug.hook.DiagnosticHook */ public Builder printDiagnosticInfo() { return listen(new ConfigurationDiagnostic()); } + /** + * Prints extensions usage help: all extension signs recognized by installers. Installers printed in + * execution order. + *

+ * Not that custom installers must provide this information by overriding + * {@link ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller#getRecognizableSigns()}. + * + * @return builder instance for chained calls + */ + public Builder printExtensionsHelp() { + return listen(new ExtensionsHelpDiagnostic()); + } + /** * Prints all registered (not disabled) installers with registration source. Useful to see all supported * extension types when multiple guicey bundles registered and available features become not obvious @@ -893,6 +995,44 @@ public Builder printGuiceAopMap(final GuiceAopConfig config) { return listen(new GuiceAopDiagnostic(config)); } + /** + * Prints guice beans construction time (during application startup) and number of created beans. + * Suitable for: + *

    + *
  • Verifying providers (and provider methods) performance + *
  • Prototype scope mis-usage (detect too many bean instantiations) + *
  • Jit bindings misuse (usually mistakes) + *
+ * Report tries to detect common mistakes: JIT binding appeared for configuration values due to missed + * qualifier annotation (with annotation it is a correct instance injection, whereas without annotation it + * would be a new JIT binding). Such situations might appear due to lombok usage: lombok generated constructor, + * but did not copy qualifier annotation from fields. + *

+ * By default, the report prints only startup time data. If you need to measure time for application runtime, + * construct diagnostic manually: + *


+         *     // false to avoid startup report
+         *     GuiceProvisionDiagnostic report = new GuiceProvisionDiagnostic(false);
+         *     // registre report
+         *     GuiceBundle....bundles(report);
+         *
+         *     // clear collected data before required point
+         *     report.clear();
+         *     ...
+         *     // generate report after measured actions
+         *     logger.info("Guice provision time {}", report.renderReport());
+         * 
+ * This might be used in tests to control guice beans creation amount and time. + *

+ * For compiled application report could be activated with: {@code -Dguicey.hooks=provision-time} + * + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.debug.hook.GuiceProvisionTimeHook for simple usage in tests + */ + public Builder printGuiceProvisionTime() { + return bundles(new GuiceProvisionDiagnostic(true)); + } + /** * Split logs with major lifecycle stage names. Useful for debugging (to better understand * at what stage your code is executed). Also, could be used for light profiling as @@ -949,6 +1089,53 @@ public Builder printJerseyConfig() { return listen(new JerseyConfigDiagnostic()); } + /** + * Prints detailed application startup (and shutdown) times. Useful for startup slowness investigations and + * initialization order validation (executed hooks and budles order). + *

+ * Time measured from guice bundle creation. Report instruments {@link io.dropwizard.core.setup.Bootstrap} + * and {@link io.dropwizard.lifecycle.setup.LifecycleEnvironment} objects to measure bunldes and managed + * objects times. + *

+ * Init time measured as time from guice bundle creation until the last dropwizard bundle init + * (bundles registered before guice bundle can't be measured; application init method time will go + * to run phase). + * Run tim measured as time from init until the last dropwizard bundle run (configuration and environment + * creation measured separately appears under run phase; application run method time will go to web phase). + * Web time is a time after run until jersey lifecycle notification (server started). + *

+ * Overall, report shows almost all application startup time with exact phases. Plus all guicey internal + * actions. + *

+ * For compiled application report could be activated with: {@code -Dguicey.hooks=startup-time} + * + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.debug.hook.StartupTimeHook + */ + public Builder printStartupTime() { + return listen(new StartupTimeDiagnostic()); + } + + /** + * Prints {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState} usage history after + * application startup. The report shows: + *

    + *
  • What objects stored in state + *
  • Who accessed stored objects (preserving access order) + *
  • Misses (requesting not yet available values) + *
  • Never set, but requested objects (including never called state value listeners). + *
+ *

+ * Note that report could be obtained at any time from the shared state object directly: + * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#getAccessReport()} + * (could be useful for resolving problems before application startup). + * + * @return builder instance for chained calls + */ + public Builder printSharedStateUsage() { + return listen(new SharedStateDiagnostic()); + } + /** * Guicey hooks ({@link GuiceyConfigurationHook}) may be loaded with system property "guicey.hooks". But * it may be not comfortable to always declare full class name (e.g. -Dguicey.hooks=com.foo.bar.Hook,..). @@ -966,16 +1153,39 @@ public Builder hookAlias(final String name, final Class + * Useful when guice modules require configuration values or when extensions registration depends on + * configuration values. + *

+ * Note: guice injector is not available yet! + *

+ * Method is not started with "on" or "listen" as other methods to differentiate this configuration + * method from pure listeners. + * + * @param runPhaseAction action to execute under run phase + * @return builder instance for chained calls + */ + public Builder whenConfigurationReady(final Consumer runPhaseAction) { + bundle.context.addDelayedConfiguration(runPhaseAction); + return this; + } + /** * Guicey manage application-wide shared state object to simplify cases when such state is required during * configuration. It may be used by bundles or hooks to "communicate". For example, server pages bundles * use this to unify global configuration. Unified place intended to replace all separate "hacks" and - * so simplify testing. Shared application state could be access statically anywhere during application + * so simplify testing. Shared application state could be accessed statically anywhere during application * life. *

* Caution: this is intended to be used only in cases when there is no other option except global state. *

- * This method could be useful for possible hook needы (maybe hooks communications) because there is no + * This method could be useful for possible hook needs (maybe hooks communications) because there is no * other way to access shared state by hooks (bundles may use special api or reference by application instance) * * @param stateAction state action @@ -986,6 +1196,113 @@ public Builder withSharedState(final Consumer stateAct return this; } + /** + * Code to execute after guice injector creation (but still under run phase). May be used for manual + * configurations (registrations into dropwizard environment). + *

+ * Listener will be called on environment command start too. + *

+ * Note: there is no registration method for this listener in main guice bundle builder + * ({@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}) because it is assumed, that such blocks would + * always be wrapped with bundles to improve application readability. + * + * @param listener listener to call after injector creation + * @param configuration type + * @return builder instance for chained calls + */ + public Builder onGuiceyStartup(final GuiceyStartupListener listener) { + return listen(new GuiceyStartupListenerAdapter<>(listener)); + } + + /** + * Code to execute after complete application startup. For server command it would happen after jetty startup + * and for lightweight guicey test helpers ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}) - + * after guicey start (as jetty not started in this case). In both cases, application completely started at + * this moment. Suitable for reporting. + *

+ * If you need to listen only for real server startup then use + * {@link #listenServer(io.dropwizard.lifecycle.ServerLifecycleListener)} instead. + *

+ * Not called on custom command execution (because no lifecycle involved in this case). In this case you can + * use {@link #onGuiceyStartup(GuiceyStartupListener)} as always executed point. + * + * @param listener listener to call on server startup + * @return builder instance for chained calls + */ + public Builder onApplicationStartup(final ApplicationStartupListener listener) { + return listen(new ApplicationStartupListenerAdapter(listener)); + } + + /** + * Code to execute after complete application shutdown. Called not only for real application but for + * environment commands and lightweight guicey test helpers + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}). Suitable for closing additional resources. + *

+ * If you need to listen only for real server shutdown then use + * {@link #listenServer(io.dropwizard.lifecycle.ServerLifecycleListener)} instead. + *

+ * Not called on command execution because no lifecycle involved in this case. + * + * @param listener listener to call on server startup + * @return builder instance for chained calls + */ + public Builder onApplicationShutdown(final ApplicationShutdownListener listener) { + return listen(new ApplicationShutdownListenerAdapter(listener)); + } + + /** + * Shortcut for {@code environment().lifecycle().addServerLifecycleListener} registration. + *

+ * Note that server listener is called only when jetty starts up and so will not be called with lightweight + * guicey test helpers {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}. Prefer using + * {@link #onApplicationStartup(ApplicationStartupListener)} to be correctly called in tests (of course, if not + * server-only execution is desired). + *

+ * Not called for custom command execution. + * + * @param listener server startup listener. + * @return builder instance for chained calls + */ + public Builder listenServer(final ServerLifecycleListener listener) { + withEnvironment(environment -> environment.lifecycle().addServerLifecycleListener(listener)); + return this; + } + + /** + * Shortcut for jetty lifecycle listener {@code environment().lifecycle().addEventListener(listener)} + * registration. + *

+ * Lifecycle listeners are called with lightweight guicey test helpers + * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp} which makes them perfectly suitable for + * reporting. + *

+ * If only startup event is required, prefer {@link #onApplicationStartup(ApplicationStartupListener)} method + * as more expressive and easier to use. + *

+ * Listeners are not called on custom command execution. + * + * @param listener jetty + * @return builder instance for chained calls + */ + public Builder listenJetty(final LifeCycle.Listener listener) { + withEnvironment(environment -> environment.lifecycle().addEventListener(listener)); + return this; + } + + /** + * Shortcut for jetty events and requests listener {@code environment().jersey().register(listener)} + * registration. + *

+ * Listeners are not called on custom command execution. + * + * @param listener listener instance + * @return builder instance for chained calls + */ + public Builder listenJersey(final ApplicationEventListener listener) { + withEnvironment(environment -> environment.jersey().register(listener)); + return this; + } + /** * @param stage stage to run injector with * @return bundle instance @@ -1001,8 +1318,17 @@ public GuiceBundle build(final Stage stage) { * @see GuiceyOptions#InjectorStage */ public GuiceBundle build() { + Preconditions.checkState(!buildCalled, + ".build() was already called for guice bundle. Most likely, it was called second time in " + + GuiceyConfigurationHook.class.getSimpleName()); + buildCalled = true; bundle.context.runHooks(this); + bundle.context.stat().stopTimer(Stat.BundleBuilderTime); return bundle; } + + private void withEnvironment(final Consumer action) { + onGuiceyStartup((config, env, injector) -> action.accept(env)); + } } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java index 5c362049f..0dc4e3425 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/GuiceyOptions.java @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice; import com.google.inject.Stage; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; import ru.vyarus.dropwizard.guice.module.context.option.Option; -import javax.servlet.DispatcherType; +import jakarta.servlet.DispatcherType; import java.util.EnumSet; /** @@ -30,11 +30,24 @@ public enum GuiceyOptions implements Option { /** * Packages for classpath scan. Not empty value indicates auto scan mode enabled. * Empty by default. + *

+ * Special value {@link ru.vyarus.dropwizard.guice.module.GuiceyInitializer#APP_PKG} might be used to configure + * application package (replaced into real package during initialization). * * @see GuiceBundle.Builder#enableAutoConfig(String...) */ ScanPackages(String[].class, new String[0]), + /** + * Enables package-private and protected (nested) classes acception by scanner (as extension). + * By default, only public extension classes are searched. + *

+ * Option exists only for rare cases when extension classes are protected by historical reasons. + * Otherwise, try to avoid using protected classes as extensions (no problems with that, just semantically not + * correct) + */ + ScanProtectedClasses(Boolean.class, false), + /** * Enables commands search in classpath and dynamic installation. Requires auto scan mode. * Disabled by default. @@ -117,6 +130,17 @@ public enum GuiceyOptions implements Option { */ AnalyzeGuiceModules(Boolean.class, true), + /** + * Support private modules analysis: in private module only exposed services could be visible. + * As guicey works with extension classes only (for simplicity), for private modules it might be required to + * add additional exposes (to expose extension binding directly). Also, for disabled extensions bindings would + * be removed inside private modules too. + *

+ * There are many different possible situations with private modules and quite possibly that not all of them + * covered. Disable this option in case of problems to avoid disabling analysis entirely. + */ + AnalyzePrivateGuiceModules(Boolean.class, true), + /** * Guice injector stage used for injector creation. * Production by default. @@ -154,13 +178,13 @@ public enum GuiceyOptions implements Option { * service by HK2 when it also depends on guice services. *

* IMPORTANT: requires extra dependency on HK2 guice-bridge: 'org.glassfish.hk2:guice-bridge:2.6.1' - * @deprecated in the next version HK2 support will be removed + *

+ * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed */ - @Deprecated UseHkBridge(Boolean.class, false); - private Class type; - private Object value; + private final Class type; + private final Object value; GuiceyOptions(final Class type, final T value) { this.type = type; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/bundle/DefaultBundleLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/DefaultBundleLookup.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/bundle/DefaultBundleLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/DefaultBundleLookup.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java index ad67eeff4..935495d64 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/GuiceyBundleLookup.java @@ -11,6 +11,7 @@ * @author Vyacheslav Rusakov * @since 15.01.2016 */ +@FunctionalInterface public interface GuiceyBundleLookup { /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java index 7146746fe..3f78e70c0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/PropertyBundleLookup.java @@ -24,6 +24,9 @@ */ public class PropertyBundleLookup implements GuiceyBundleLookup { + /** + * Bundles system property. + */ public static final String BUNDLES_PROPERTY = "guicey.bundles"; @Override diff --git a/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/ServiceLoaderBundleLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/ServiceLoaderBundleLookup.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/ServiceLoaderBundleLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/ServiceLoaderBundleLookup.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/VoidBundleLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/VoidBundleLookup.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/VoidBundleLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/bundle/lookup/VoidBundleLookup.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java index 9177ba5f3..753fe069c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ConfigurationDiagnostic.java @@ -116,6 +116,9 @@ public int hashCode() { return reportTitle.hashCode(); } + /** + * @return diagnostic report configuration builder + */ public static Builder builder() { return new Builder("Diagnostic report"); } @@ -150,6 +153,11 @@ public static class Builder { private DiagnosticConfig config; private ContextTreeConfig treeConfig; + /** + * Create a report builder. + * + * @param reportTitle report title + */ public Builder(final String reportTitle) { this.reportTitle = reportTitle; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpDiagnostic.java new file mode 100644 index 000000000..d84efc19a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpDiagnostic.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.debug; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.debug.report.extensions.ExtensionsHelpRenderer; +import ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InstallersResolvedEvent; + +/** + * Guicey extensions help: shows all extension recognition signs (for installers, providing this info). + *

+ * In order to support this report, installer must implement + * {@link ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller#getRecognizableSigns()} (default interface + * method). + *

+ * If multiple reports would be registered only one instance would be accepted (to prevent report duplications). + * + * @author Vyacheslav Rusakov + * @since 06.12.2022 + */ +public class ExtensionsHelpDiagnostic extends UniqueGuiceyLifecycleListener { + private final Logger logger = LoggerFactory.getLogger(ExtensionsHelpDiagnostic.class); + + @Override + protected void installersResolved(final InstallersResolvedEvent event) { + logger.info("Recognized extension signs" + + new ExtensionsHelpRenderer(event.getInstallers()).renderReport(null)); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java index 341f304d1..0967bbab4 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceAopDiagnostic.java @@ -21,6 +21,11 @@ public class GuiceAopDiagnostic extends GuiceyLifecycleAdapter { private final Logger logger = LoggerFactory.getLogger(GuiceAopDiagnostic.class); private final GuiceAopConfig config; + /** + * Create AOP diagnostic report. + * + * @param config report config + */ public GuiceAopDiagnostic(final GuiceAopConfig config) { this.config = config; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java index d198e3cc0..3220068b7 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceBindingsDiagnostic.java @@ -23,6 +23,11 @@ public class GuiceBindingsDiagnostic extends UniqueGuiceyLifecycleListener { private final Logger logger = LoggerFactory.getLogger(GuiceBindingsDiagnostic.class); private final GuiceConfig config; + /** + * Create bindings report. + * + * @param config report config + */ public GuiceBindingsDiagnostic(final GuiceConfig config) { this.config = config; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceProvisionDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceProvisionDiagnostic.java new file mode 100644 index 000000000..e5d19e0bd --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/GuiceProvisionDiagnostic.java @@ -0,0 +1,128 @@ +package ru.vyarus.dropwizard.guice.debug; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.matcher.Matchers; +import com.google.inject.spi.ProvisionListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceProvisionRenderer; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; + +import java.time.Duration; + +/** + * Guice beans creation (provision) time diagnostic. Records all requested beans creation time. By default, + * prints collected data after application startup (most of the beans created at startup). + *

+ * The report shows: + *

    + *
  • Each bean creation time (mostly useful for providers, which might be slow) + *
  • Number of created instances (for each type) + *
  • All requested bindings, including JIT bindings - useful for unexpected JIT bindings detection + *
  • Detects if the same type is requested with and without qualifier - usually this means incorrect usage + * (forgotten qualifier), but it might not be obvious (as guice create JIT binding in this case). + *
+ * The report is sorted by overall spent time: if guice bean (in prototype) scope was created several times - the summ + * of all creations is counted. + *

+ * The report also could be used to measure runtime creations: + *


+ *     // false to avoid startup report
+ *     GuiceProvisionDiagnostic report = new GuiceProvisionDiagnostic(false);
+ *     // registre report
+ *     GuiceBundle....bundles(report);
+ *
+ *     // clear collected data before required point
+ *     report.clear();
+ *     // do something requiring new beans creation
+ *     injector.getInstance(JitService.class); // just an example
+ *
+ *     // generate report after measured actions
+ *     logger.info("Guice provision time {}", report.renderReport());
+ * 
+ * + * @author Vyacheslav Rusakov + * @since 24.03.2025 + */ +public class GuiceProvisionDiagnostic implements GuiceyBundle { + private final Logger logger = LoggerFactory.getLogger(GuiceProvisionDiagnostic.class); + private final ListMultimap, Duration> data = LinkedListMultimap.create(); + + private final boolean printStartupReport; + + /** + * Create report. + * + * @param printStartupReport true to print report after application startup + */ + public GuiceProvisionDiagnostic(final boolean printStartupReport) { + this.printStartupReport = printStartupReport; + } + + @Override + public void run(final GuiceyEnvironment environment) throws Exception { + environment.modules(new ProvisionListenerModule(data)); + if (printStartupReport) { + environment.onApplicationStartup(injector -> + logger.info("Guice bindings provision time: {}", renderReport())); + } + } + + /** + * Clear collected data (to record new data at runtime). + */ + public void clear() { + data.clear(); + } + + /** + * Map format: binding - provisions time. + * + * @return recorded provision data + */ + public ListMultimap, Duration> getRecordedData() { + return LinkedListMultimap.create(data); + } + + /** + * @return generated report for collected data + */ + public String renderReport() { + return new GuiceProvisionRenderer().render(LinkedListMultimap.create(data)); + } + + /** + * Module records guice beans provision times. + */ + public static class ProvisionListenerModule extends AbstractModule { + + private final Multimap, Duration> provisions; + + /** + * Create module. + * + * @param provisions provisions collector + */ + public ProvisionListenerModule(final Multimap, Duration> provisions) { + this.provisions = provisions; + } + + @Override + protected void configure() { + bindListener(Matchers.any(), new ProvisionListener() { + @Override + public void onProvision(final ProvisionInvocation provision) { + final Stopwatch stopwatch = Stopwatch.createStarted(); + provision.provision(); + provisions.put(provision.getBinding(), stopwatch.stop().elapsed()); + } + }); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java index cf3957953..8a8d18bac 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/JerseyConfigDiagnostic.java @@ -25,7 +25,7 @@ public class JerseyConfigDiagnostic extends UniqueGuiceyLifecycleListener { @Override protected void applicationStarted(final ApplicationStartedEvent event) { - if (!event.isJettyStarted()) { + if (!event.isJerseyStarted()) { // report can't be shown in lightweight tests (jersey injector not started) return; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java index d00a2c348..e3d2e0160 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/LifecycleDiagnostic.java @@ -49,7 +49,7 @@ * @author Vyacheslav Rusakov * @since 17.04.2018 */ -@SuppressWarnings({"checkstyle:ClassFanOutComplexity", "PMD.TooManyMethods", "PMD.ExcessiveImports"}) +@SuppressWarnings({"checkstyle:ClassFanOutComplexity", "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects"}) public class LifecycleDiagnostic extends UniqueGuiceyLifecycleListener { private static final String BUNDLES = "bundles"; @@ -64,6 +64,11 @@ public class LifecycleDiagnostic extends UniqueGuiceyLifecycleListener { // counting time from listener creation (~same as bundle registration and app initial configuration) private final StopWatch timer = StopWatch.createStarted(); + /** + * Create lifecycle diagnostic. + * + * @param showDetails true to show all details (verbose) + */ public LifecycleDiagnostic(final boolean showDetails) { this.showDetails = showDetails; } @@ -125,18 +130,6 @@ protected void installersResolved(final InstallersResolvedEvent event) { } } - @Override - protected void manualExtensionsValidated(final ManualExtensionsValidatedEvent event) { - log("%s manual extensions validated (of %s registered)", - event.getValidated().size(), event.getExtensions().size()); - if (showDetails) { - logDetails("validated", event.getValidated()); - final List> ignored = new ArrayList<>(event.getExtensions()); - ignored.removeAll(event.getValidated()); - logDetails("ignored", ignored); - } - } - @Override protected void classpathExtensionsResolved(final ClasspathExtensionsResolvedEvent event) { log("%s classpath extensions detected", event.getExtensions().size()); @@ -153,6 +146,18 @@ protected void bundlesStarted(final BundlesStartedEvent event) { } } + @Override + protected void manualExtensionsValidated(final ManualExtensionsValidatedEvent event) { + log("%s manual extensions validated (of %s registered)", + event.getValidated().size(), event.getExtensions().size()); + if (showDetails) { + logDetails("validated", event.getValidated()); + final List> ignored = new ArrayList<>(event.getExtensions()); + ignored.removeAll(event.getValidated()); + logDetails("ignored", ignored); + } + } + @Override protected void modulesAnalyzed(final ModulesAnalyzedEvent event) { log("%s binding extensions detected", event.getExtensions().size()); @@ -165,7 +170,7 @@ protected void modulesAnalyzed(final ModulesAnalyzedEvent event) { for (Binding binding : event.getBindingsRemoved()) { final List modules = BindingUtils.getModules(binding).stream() .sorted(Collections.reverseOrder()) - .map(it -> it.substring(it.lastIndexOf(".") + 1)) + .map(it -> it.substring(it.lastIndexOf('.') + 1)) .collect(Collectors.toList()); bindings.add(String.join("/", modules) + " | " + RenderUtils .renderClassLine(binding.getKey().getTypeLiteral().getRawType())); @@ -249,9 +254,9 @@ private void logDetails(final String message, final Collection items) { .append('\t').append(message).append(" = \n"); for (Object item : items) { builder.append("\t\t").append(item instanceof String ? item - // it is the only way to show something meaningful for proxy - : (item instanceof Proxy ? item.toString() - : RenderUtils.renderClassLine(item instanceof Class ? (Class) item : item.getClass()))) + // it is the only way to show something meaningful for proxy + : (item instanceof Proxy ? item.toString() + : RenderUtils.renderClassLine(item instanceof Class ? (Class) item : item.getClass()))) .append(NL); } System.out.println(builder.toString()); @@ -260,7 +265,7 @@ private void logDetails(final String message, final Collection items) { /** * Jetty listener. */ - private class JettyLifecycleListener implements LifeCycle.Listener { + private final class JettyLifecycleListener implements LifeCycle.Listener { @Override public void lifeCycleStarting(final LifeCycle event) { log("Jetty starting..."); @@ -286,9 +291,9 @@ public void lifeCycleStopped(final LifeCycle event) { /** * Jersey listener. */ - private class JerseyEventListener implements ApplicationEventListener { + private final class JerseyEventListener implements ApplicationEventListener { @Override - @SuppressWarnings({"checkstyle:MissingSwitchDefault", "PMD.SwitchStmtsShouldHaveDefault"}) + @SuppressWarnings({"checkstyle:MissingSwitchDefault", "PMD.NonExhaustiveSwitch"}) @SuppressFBWarnings("SF_SWITCH_NO_DEFAULT") public void onEvent(final ApplicationEvent event) { switch (event.getType()) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnostic.java new file mode 100644 index 000000000..7157b0ba7 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnostic.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.debug; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; + +/** + * Prints shared state usage during application startup. Shows: + *
    + *
  • What objects stored in state + *
  • Who accessed stored objects (preserving access order) + *
  • Misses (requesting not yet available values) + *
  • Never set, but requested objects (including never called state value listeners). + *
+ * + * @author Vyacheslav Rusakov + * @since 20.03.2025 + */ +public class SharedStateDiagnostic extends UniqueGuiceyLifecycleListener { + private final Logger logger = LoggerFactory.getLogger(SharedStateDiagnostic.class); + + @Override + protected void applicationStarted(final ApplicationStartedEvent event) { + logger.info("Shared configuration state usage: \n{}", event.getSharedState().getAccessReport()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/StartupTimeDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/StartupTimeDiagnostic.java new file mode 100644 index 000000000..da4b26c03 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/StartupTimeDiagnostic.java @@ -0,0 +1,175 @@ +package ru.vyarus.dropwizard.guice.debug; + +import com.google.common.base.Stopwatch; +import org.eclipse.jetty.util.component.LifeCycle; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.debug.report.start.DropwizardBundlesTracker; +import ru.vyarus.dropwizard.guice.debug.report.start.ManagedTracker; +import ru.vyarus.dropwizard.guice.debug.report.start.ShutdownTimeInfo; +import ru.vyarus.dropwizard.guice.debug.report.start.ShutdownTimeRenderer; +import ru.vyarus.dropwizard.guice.debug.report.start.StartupTimeInfo; +import ru.vyarus.dropwizard.guice.debug.report.start.StartupTimeRenderer; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.module.context.ConfigItem; +import ru.vyarus.dropwizard.guice.module.context.info.ItemId; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsInfo; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BeforeInitEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesInitializedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.run.ApplicationRunEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.run.BeforeRunEvent; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Startup time report. Timers could count only time AFTER guice bundle creation (with guice bundle itself). + *

+ * Report hacks dropwizard bundles and managed objects to track all bundles time. Note that it is impossible to + * track init time of bundles registered before guice bundle. + *

+ * Entire application startup time (measured since guice bundle creation) is split into 3 chunks: + *

    + *
  • init phase - all dropwizard bundle init methods called + *
  • run phase - all dropwizard bundles run methods called (+ time of Configuration and Environment creation); + * also includes application init method + *
  • web phase - everything after last dropwizard bundle run (including application run method) + *
+ *

+ * To avoid confusion with server startup time - jvm start time is also shown (time from jvm start and before + * guice bundle creation). + * + * @author Vyacheslav Rusakov + * @since 07.03.2025 + */ +@SuppressWarnings({"ClassDataAbstractionCoupling", "ClassFanOutComplexity", "PMD.ExcessiveImports"}) +public class StartupTimeDiagnostic extends UniqueGuiceyLifecycleListener { + private final Logger logger = LoggerFactory.getLogger(StartupTimeDiagnostic.class); + + private final StartupTimeInfo start = new StartupTimeInfo(); + private final ShutdownTimeInfo stop = new ShutdownTimeInfo(); + + // for tracking dw phases in bundles tracker + @SuppressWarnings("PMD.LooseCoupling") + private DropwizardBundlesTracker bundlesTracker; + + @Override + protected void beforeInit(final BeforeInitEvent event) { + bundlesTracker = new DropwizardBundlesTracker(event.getStats(), start, event.getBootstrap()); + } + + @Override + protected void bundlesInitialized(final BundlesInitializedEvent event) { + // store bundles init order to show them correctly + start.setGuiceyBundlesInitOrder(event.getBundles().stream() + .map(GuiceyBundle::getClass) + .collect(Collectors.toList())); + } + + // just before guicey run + @Override + protected void beforeRun(final BeforeRunEvent event) { + // for tracking managed objects execution + new ManagedTracker(start, stop, event.getEnvironment().lifecycle()); + final Stopwatch jerseyTime = Stopwatch.createUnstarted(); + // listener would be called on normal run and for grizzly 2 rest stubs + event.getEnvironment().jersey().register(new ApplicationEventListener() { + @Override + public void onEvent(final ApplicationEvent applicationEvent) { + final ApplicationEvent.Type type = applicationEvent.getType(); + if (type == ApplicationEvent.Type.INITIALIZATION_START) { + jerseyTime.start(); + } else if (type == ApplicationEvent.Type.INITIALIZATION_FINISHED) { + start.setJerseyTime(jerseyTime.elapsed()); + } + } + + @Override + public RequestEventListener onRequest(final RequestEvent requestEvent) { + return null; + } + }); + } + + // guicey run done + @Override + @SuppressWarnings("AnonInnerLength") + protected void applicationRun(final ApplicationRunEvent event) { + // remember transitive bundles to correctly calculate each bundle time + event.getConfigurationInfo().getGuiceyBundleIds().forEach(id -> { + final List> transitive = event.getConfigurationInfo().getData() + .getItems(itemInfo -> ConfigItem.Bundle.equals(itemInfo.getItemType()) + && itemInfo.getRegistrationScope().equals(id)) + .stream().map(ItemId::getType).collect(Collectors.toList()); + if (!transitive.isEmpty()) { + start.getGuiceyBundleTransitives().putAll(id.getType(), transitive); + } + }); + // apply custom listener instead of guicey events to run AFTER all guicey events + event.registerJettyListener(new JettyListener(event)); + } + + private class JettyListener implements LifeCycle.Listener { + + private final Stopwatch startTime; + private final Stopwatch stopTime; + private final List> startupEvents; + private Duration startListenersTime; + private final StatsInfo stats; + + JettyListener(final ApplicationRunEvent event) { + startTime = Stopwatch.createUnstarted(); + stopTime = Stopwatch.createUnstarted(); + startupEvents = new ArrayList<>(); + stats = event.getInjector().getInstance(GuiceyConfigurationInfo.class).getStats(); + } + + @Override + public void lifeCycleStarting(final LifeCycle event) { + startTime.start(); + } + + @Override + public void lifeCycleStarted(final LifeCycle evt) { + // web time - everything after last bundle run + // OverallTime stat will get called a bit earlier (cause listener registered earlier, so using custom + // timer for more accurate value) + start.setWebTime(bundlesTracker.getWebTimer().stop().elapsed()); + start.setLifecycleTime(startTime.elapsed()); + start.setStats(stats); + startupEvents.addAll(stats.getDetailedStats(DetailStat.Listener).keySet()); + startListenersTime = stats.duration(Stat.ListenersTime); + start.getWebEvents().addAll(startupEvents); + start.getWebEvents().removeAll(start.getInitEvents()); + start.getWebEvents().removeAll(start.getRunEvents()); + logger.info("Application startup time: {}", new StartupTimeRenderer().render(start)); + } + + @Override + public void lifeCycleStopping(final LifeCycle event) { + stopTime.start(); + } + + @Override + public void lifeCycleStopped(final LifeCycle event) { + stop.setStopTime(stopTime.stop().elapsed()); + stop.getEvents().addAll(stats.getDetailedStats(DetailStat.Listener).keySet()); + stop.getEvents().removeAll(startupEvents); + final Duration shutdownListeners = stats.duration(Stat.ListenersTime); + stop.setListenersTime(startListenersTime != null ? shutdownListeners.minus(startListenersTime) + : shutdownListeners); + stop.setStats(stats); + logger.info("Application shutdown time: {}", new ShutdownTimeRenderer().render(stop)); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java index 5ce719a4c..31d9fee8e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/WebMappingsDiagnostic.java @@ -26,6 +26,11 @@ public class WebMappingsDiagnostic extends UniqueGuiceyLifecycleListener { private final MappingsConfig config; + /** + * Create diagnostic. + * + * @param config config + */ public WebMappingsDiagnostic(final MappingsConfig config) { this.config = config; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java index 5b7f21258..a3f9d60e8 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/YamlBindingsDiagnostic.java @@ -27,12 +27,20 @@ public class YamlBindingsDiagnostic extends UniqueGuiceyLifecycleListener { private final BindingsConfig config; + /** + * Create diagnostic. + */ public YamlBindingsDiagnostic() { this(new BindingsConfig() .showConfigurationTree() .showNullValues()); } + /** + * Create diagnostic. + * + * @param config configuration + */ public YamlBindingsDiagnostic(final BindingsConfig config) { this.config = config; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java similarity index 88% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java index cfb4bc197..f2244c86a 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/DiagnosticHook.java @@ -12,6 +12,11 @@ */ public class DiagnosticHook implements GuiceyConfigurationHook { + /** + * Hook system property alias. + */ + public static final String ALIAS = "diagnostic"; + @Override public void configure(final GuiceBundle.Builder builder) { builder.printDiagnosticInfo() diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/GuiceProvisionTimeHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/GuiceProvisionTimeHook.java new file mode 100644 index 000000000..9f84c6b41 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/GuiceProvisionTimeHook.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.debug.hook; + +import com.google.common.collect.ListMultimap; +import com.google.inject.Binding; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.time.Duration; + +/** + * Hook enables guice provision time logs. It is assumed to be used to enable provision time logs for compiled + * application with system property: {@code -Dguicey.hooks=provision-time}. + *

+ * Also, hook could be used in tests to track created guice beans: + *


+ *   {@literal @}EnableHook
+ *    static GuiceProvisionTimeHook hook = new GuiceProvisionTimeHook();
+ *
+ *   {@literal @}Test
+ *    public void test() {
+ *        hook.clearData()
+ *        // anything requiring provision
+ *        injector.getInstance(SomeService.class);
+ *        // the report would contain only one bean creation
+ *        System.out.println(hook.renderReport())
+ *    }
+ * 
+ * + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class GuiceProvisionTimeHook implements GuiceyConfigurationHook { + + /** + * System property hook alias name. + */ + public static final String ALIAS = "provision-time"; + private final GuiceProvisionDiagnostic diagnostic = new GuiceProvisionDiagnostic(true); + + @Override + public void configure(final GuiceBundle.Builder builder) { + // this is the same as .printGuiceProvisionTime() call, but hook could be used in tests + builder.bundles(diagnostic); + } + + /** + * Clear collected data. + */ + public void clearData() { + diagnostic.clear(); + } + + /** + * Map format: binding - provisions time. + * + * @return recorded provision data + */ + public ListMultimap, Duration> getRecordedData() { + return diagnostic.getRecordedData(); + } + + /** + * @return render beans creation report from collected data + */ + public String renderReport() { + return diagnostic.renderReport(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/StartupTimeHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/StartupTimeHook.java new file mode 100644 index 000000000..79f28ccb3 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/hook/StartupTimeHook.java @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.debug.hook; + +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +/** + * Hook enables startup time logs. It is assumed to be used to enable startup time logs for compiled application + * with the system property: {@code -Dguicey.hooks=startup-time}. + * + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ +public class StartupTimeHook implements GuiceyConfigurationHook { + + /** + * Hook alias for system property. + */ + public static final String ALIAS = "startup-time"; + + @Override + public void configure(final GuiceBundle.Builder builder) { + builder.printStartupTime(); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java index 93defd1bf..a956c4933 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/ReportRenderer.java @@ -7,6 +7,7 @@ * @author Vyacheslav Rusakov * @since 01.08.2016 */ +@FunctionalInterface public interface ReportRenderer { /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticConfig.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticConfig.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java similarity index 98% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java index 2baf80478..e15dd42e2 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/diagnostic/DiagnosticRenderer.java @@ -2,8 +2,8 @@ import com.google.common.collect.Lists; import com.google.inject.Module; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.cli.Command; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.cli.Command; import ru.vyarus.dropwizard.guice.debug.report.ReportRenderer; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; @@ -87,6 +87,11 @@ public class DiagnosticRenderer implements ReportRenderer { private final GuiceyConfigurationInfo service; + /** + * Create renderer. + * + * @param service info service + */ public DiagnosticRenderer(final GuiceyConfigurationInfo service) { this.service = service; } @@ -151,7 +156,7 @@ private void printBundles(final DiagnosticConfig config, final StringBuilder res final List dwMarker = Collections.singletonList(DW); for (Class bundle : service.getBundlesDisabled()) { res.append(TAB).append(TAB).append(renderDisabledClassLine(bundle, 0, - ConfiguredBundle.class.isAssignableFrom(bundle) ? dwMarker : null)) + ConfiguredBundle.class.isAssignableFrom(bundle) ? dwMarker : null)) .append(NEWLINE); } } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/extensions/ExtensionsHelpRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/extensions/ExtensionsHelpRenderer.java new file mode 100644 index 000000000..dfe90c39b --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/extensions/ExtensionsHelpRenderer.java @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.debug.report.extensions; + +import ru.vyarus.dropwizard.guice.debug.report.ReportRenderer; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; + +import java.util.List; + +import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.NEWLINE; +import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.TAB; + +/** + * Renders known extension signs. Signs grouped by installer. Installers ordered in execution order. + *

+ * Note: correct information could be shown only for installers explicitly providing this information + * (by overriding {@link ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller#getRecognizableSigns()}). + * + * @author Vyacheslav Rusakov + * @since 06.12.2022 + */ +public class ExtensionsHelpRenderer implements ReportRenderer { + private final List installers; + + /** + * Create renderer. + * + * @param installers installers + */ + public ExtensionsHelpRenderer(final List installers) { + this.installers = installers; + } + + @Override + @SuppressWarnings("unchecked") + public String renderReport(final Void config) { + final StringBuilder res = new StringBuilder(NEWLINE); + for (FeatureInstaller installer : installers) { + final Class instType = (Class) installer.getClass(); + res.append(NEWLINE).append(TAB).append(RenderUtils.renderInstaller(instType, null)).append(NEWLINE); + final List signs = installer.getRecognizableSigns(); + for (String sign : signs) { + res.append(TAB).append(TAB).append(sign).append(NEWLINE); + } + } + return res.toString(); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopConfig.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopConfig.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java similarity index 98% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java index c9e284044..9add73b11 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceAopMapRenderer.java @@ -1,6 +1,7 @@ package ru.vyarus.dropwizard.guice.debug.report.guice; // CHECKSTYLE:OFF -import com.google.inject.Module; // NOPMD + +import com.google.inject.Module; // CHECKSTYLE:ON import com.google.inject.*; import com.google.inject.spi.ConstructorBinding; @@ -37,6 +38,11 @@ public class GuiceAopMapRenderer implements ReportRenderer { private final Injector injector; private final List modules; + /** + * Create renderer. + * + * @param injector injector + */ public GuiceAopMapRenderer(final Injector injector) { this.injector = injector; final GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java index 8bcc7d39c..73a10f428 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceBindingsRenderer.java @@ -73,6 +73,11 @@ public class GuiceBindingsRenderer implements ReportRenderer { private final List> modulesDisabled; private final boolean analysisEnabled; + /** + * Create bindings renderer. + * + * @param injector injector + */ public GuiceBindingsRenderer(final Injector injector) { this.injector = injector; final GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); @@ -171,7 +176,7 @@ private void renderJitBindings(final StringBuilder res, jitBindings.remove(dec.getKey()); } // remove "JIT" bindings from key declaration: bind(A.class).to(B.class) will lead to jit binding creation - // for B class, but we dont need to know about it - only pure JIT bindings required + // for B class, but we don't need to know about it - only pure JIT bindings required if (dec.getTarget() != null) { jitBindings.remove(dec.getTarget()); } @@ -236,7 +241,7 @@ private void renderBindingChains(final StringBuilder res, private List filter(final List modules, final GuiceConfig config) { modules.removeIf(it -> config.getIgnoreModules().contains(it.getType()) - || (!it.isJITBindings() && filter(it.getType().getName(), config.getIgnorePackages()))); + || (!it.isJitModule() && filter(it.getType().getName(), config.getIgnorePackages()))); for (ModuleDeclaration mod : modules) { if (modulesDisabled.contains(mod.getType())) { // ignore removed module's subtree @@ -284,7 +289,6 @@ private void markExtensions(final Map moduleBindings) { } @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_INFERRED") - @SuppressWarnings({"PMD.AvoidInstantiatingObjectsInLoops", "PMD.ConsecutiveLiteralAppends"}) private void render(final TreeNode root, final ModuleDeclaration mod) { final TreeNode next = root.child(RenderUtils.renderClassLine(mod.getType(), mod.getMarkers())); @@ -340,7 +344,7 @@ private String renderElement(final BindingDeclaration declaration) { return res; } - @SuppressWarnings({"unchecked", "PMD.AvoidInstantiatingObjectsInLoops"}) + @SuppressWarnings({"unchecked", "PMD.CognitiveComplexity"}) private List renderChainLines(final List roots, final Map bindings) { final List lines = new ArrayList<>(); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceConfig.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceConfig.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceProvisionRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceProvisionRenderer.java new file mode 100644 index 000000000..dbf1b052d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/GuiceProvisionRenderer.java @@ -0,0 +1,204 @@ +package ru.vyarus.dropwizard.guice.debug.report.guice; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Binding; +import ru.vyarus.dropwizard.guice.debug.report.guice.model.BindingDeclaration; +import ru.vyarus.dropwizard.guice.debug.report.guice.util.GuiceModelUtils; +import ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor.GuiceBindingVisitor; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; +import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; +import ru.vyarus.java.generics.resolver.util.map.EmptyGenericsMap; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Renders guice beans provision (creation) time. The report shows all created beans (including multiple times) + * with all related binding keys. The report highlights JIT bindings to simplify searching for injection point + * declaration mistakes (forgotten qualifier). + *

+ * The report tries to guess incorrect JIT bindings: if there are qualified bindings (with annotation or generic) + * of the same type exists, then such JIT binding considered suspicious and rendered before the main report. + * + * @author Vyacheslav Rusakov + * @since 24.03.2025 + */ +public class GuiceProvisionRenderer { + private static final GuiceBindingVisitor BINDING_VISITOR = new GuiceBindingVisitor(); + private static final int MAX_PROVISIONS = 5; + + /** + * Render provisions report. + * + * @param provisions collected provisions data + * @return rendered report + */ + public String render(final ListMultimap, Duration> provisions) { + final List parsed = process(provisions); + final StringBuilder res = new StringBuilder(1000); + res.append('\n'); + + // reveal mappings of the same type, but with different modifiers + // (as example, injecting a configuration object without @Config annotation) + final Multimap, Provision> suspicious = findSuspicious(parsed); + if (!suspicious.isEmpty()) { + res.append("\n\tPossible mistakes (unqualified JIT bindings):\n"); + + suspicious.keySet().stream() + .sorted(Comparator.comparing(Class::getSimpleName)) + .forEach(type -> { + res.append("\n\t\t @Inject ") + .append(TypeToStringUtils.toStringType(type, EmptyGenericsMap.getInstance())) + .append(":\n"); + + suspicious.get(type).stream() + .sorted(Comparator.comparing(Provision::getKey)) + .forEach(provision -> res + .append(String.format("\t\t\t%s ", + provision.getDeclaration().getSource() == null ? '>' : ' ')) + .append(renderProvision("", provision))); + }); + } + + res.append("\n\tOverall ").append(countBeans(parsed)).append(" provisions took ") + .append(PrintUtils.ms(countOverall(parsed))).append('\n'); + for (Provision provision : parsed) { + res.append(renderProvision("\t\t", provision)); + } + + return res.toString(); + } + + private List process(final ListMultimap, Duration> provisions) { + final List res = new ArrayList<>(); + + for (Binding binding : provisions.keySet()) { + final BindingDeclaration declaration = binding.acceptTargetVisitor(BINDING_VISITOR); + declaration.setScope(GuiceModelUtils.getScope(binding)); + declaration.setSource(GuiceModelUtils.renderSource(binding)); + + res.add(new Provision(GuiceModelUtils.renderKey(binding.getKey()), + binding, new ArrayList<>(provisions.get(binding)), declaration)); + } + + // the slowest provisions go first + // in case of multiple provisions, the first one would be the slowest + final Comparator comparing = Comparator + .comparing(Provision::getOverall); + res.sort(comparing.reversed()); + + return res; + } + + private Duration countOverall(final List provisions) { + Duration total = Duration.ZERO; + for (Provision provision : provisions) { + total = total.plus(provision.getOverall()); + } + return total; + } + + private int countBeans(final List provisions) { + int total = 0; + for (Provision provision : provisions) { + total += provision.getProvisions().size(); + } + return total; + } + + @SuppressWarnings("PMD.UseStringBufferForStringAppends") + private String renderProvision(final String prefix, final Provision provision) { + final BindingDeclaration dec = provision.getDeclaration(); + String time = PrintUtils.ms(provision.getOverall()); + if (provision.getProvisions().size() > 1) { + time += " (" + provision.getProvisions().stream() + .limit(MAX_PROVISIONS) + .map(PrintUtils::ms) + .collect(Collectors.joining(" + ")); + if (provision.getProvisions().size() > MAX_PROVISIONS) { + time += " + ..."; + } + time += ")"; + } + return String.format("%s%-20s %-16s %-80s %-4s : %-10s \t\t %s%n", + prefix, + dec.getSource() == null ? "JIT" : dec.getType().name().toLowerCase(), + dec.getScope() != null ? "[@" + dec.getScope().getSimpleName() + ']' : "", + provision.getKey(), + provision.getProvisions().size() > 1 ? "x" + provision.getProvisions().size() : "", + time, + dec.getSource() != null ? dec.getSource() : ""); + } + + private Multimap, Provision> findSuspicious(final List provisions) { + final Multimap, Provision> res = HashMultimap.create(); + + provisions.forEach(provision -> + res.put(provision.getBinding().getKey().getTypeLiteral().getRawType(), provision)); + + res.keySet().removeIf(type -> { + if (res.get(type).size() == 1) { + return true; + } + + // at least one is a jit binding (others would be annotated) + boolean notAnnotated = true; + for (Provision provision : res.get(type)) { + notAnnotated = notAnnotated + // jit with annotation is not possible + && provision.getBinding().getKey().getAnnotation() != null; + } + return notAnnotated; + }); + + return res; + } + + private static class Provision { + private final String key; + private final Binding binding; + private final List provisions; + private final BindingDeclaration declaration; + private final Duration overall; + + Provision(final String key, + final Binding binding, + final List provisions, + final BindingDeclaration declaration) { + this.key = key; + this.binding = binding; + this.provisions = provisions; + this.declaration = declaration; + Duration overall = Duration.ZERO; + for (Duration duration : provisions) { + overall = overall.plus(duration); + } + this.overall = overall; + } + + public String getKey() { + return key; + } + + public Binding getBinding() { + return binding; + } + + public List getProvisions() { + return provisions; + } + + public BindingDeclaration getDeclaration() { + return declaration; + } + + public Duration getOverall() { + return overall; + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java similarity index 72% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java index bf2377694..1c4832ae9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/BindingDeclaration.java @@ -34,6 +34,12 @@ public class BindingDeclaration { private final List markers = new ArrayList<>(); private String module; + /** + * Create binding declaration. + * + * @param type binding type + * @param element binding element + */ public BindingDeclaration(final DeclarationType type, final Object element) { Preconditions.checkState(type.getType().isAssignableFrom(element.getClass()), "%s requires %s, but %s binding provided", type.name(), @@ -42,84 +48,141 @@ public BindingDeclaration(final DeclarationType type, final Object element) { this.element = element; } + /** + * @return binding type + */ public DeclarationType getType() { return type; } + /** + * @return binding element + */ public Object getElement() { return element; } + /** + * @return binding key + */ public Key getKey() { return key; } + /** + * @param key binding key + */ public void setKey(final Key key) { this.key = key; } + /** + * @return target key for linked bindings or null + */ public Key getTarget() { return target; } + /** + * @param target target key + */ public void setTarget(final Key target) { this.target = target; } + /** + * @return provider key (render) or another provider identity, otherwise null + */ public String getProvidedBy() { return providedBy; } + /** + * @param providedBy rendered provider key or another provider identity + */ public void setProvidedBy(final String providedBy) { this.providedBy = providedBy; } + /** + * @return binding scope + */ public Class getScope() { return scope; } + /** + * @param scope binding scope + */ public void setScope(final Class scope) { this.scope = scope; } + /** + * @return binding declaration source + */ public String getSource() { return source; } + /** + * @param source binding declaration source + */ public void setSource(final String source) { this.source = source; } + /** + * @return binding declaration source line + */ public int getSourceLine() { return sourceLine; } + /** + * @param sourceLine binding declaration source line + */ public void setSourceLine(final int sourceLine) { this.sourceLine = sourceLine; } + /** + * @return additional binding data (type listener, provision listener or aop interceptors declaration bindings. + * etc.) + */ public List getSpecial() { return special; } + /** + * @param special special binding data + */ public void setSpecial(final List special) { this.special = special; } + /** + * @return binding markers (OVERRIDE, OVERRIDDEN, EXTENSION, REMOVED) + */ public List getMarkers() { return markers; } + /** + * @return module name + */ public String getModule() { return module; } + /** + * @param module module name + */ public void setModule(final String module) { this.module = module; } @Override - @SuppressWarnings("PMD.UseStringBufferForStringAppends") public String toString() { String res = type.name().toLowerCase() + " " + GuiceModelUtils.renderKey(key); if (module != null) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/DeclarationType.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/DeclarationType.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/DeclarationType.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/DeclarationType.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java similarity index 79% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java index e84a19d4d..fb9197bd0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/model/ModuleDeclaration.java @@ -21,42 +21,65 @@ public class ModuleDeclaration { private final List markers = new ArrayList<>(); private boolean privateModule; + /** + * @return module type + */ public Class getType() { return type; } + /** + * @param type module type + */ public void setType(final Class type) { this.type = type; } + /** + * @return parent module or null + */ public String getParent() { return parent; } + /** + * @param parent parent module + */ public void setParent(final String parent) { this.parent = parent; } + /** + * @return children modules + */ public List getChildren() { return children; } + /** + * @return declared bindings + */ public List getDeclarations() { return declarations; } + /** + * @return module markers + */ public List getMarkers() { return markers; } - public boolean isJITBindings() { - return Module.class.equals(type); - } - + /** + * @return true for private module + */ public boolean isPrivateModule() { return privateModule; } + /** + * @param privateModule true for private module + */ public void setPrivateModule(final boolean privateModule) { this.privateModule = privateModule; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java index 6bb871f49..684579b5f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelParser.java @@ -9,19 +9,23 @@ import com.google.inject.spi.Element; import com.google.inject.spi.ElementSource; import com.google.inject.spi.PrivateElements; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.debug.report.guice.model.BindingDeclaration; import ru.vyarus.dropwizard.guice.debug.report.guice.model.ModuleDeclaration; import ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor.GuiceElementVisitor; -import ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor.GuiceScopingVisitor; import ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor.PrivateModuleException; -import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; import ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils; -import java.lang.annotation.Annotation; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; /** * Guice introspection utils. Parse guice SPI model. Supports both elements from modules and injector bindings. @@ -33,7 +37,6 @@ public final class GuiceModelParser { private static final Logger LOGGER = LoggerFactory.getLogger(GuiceModelParser.class); - private static final GuiceScopingVisitor SCOPE_DETECTOR = new GuiceScopingVisitor(); private static final GuiceElementVisitor ELEMENT_VISITOR = new GuiceElementVisitor(); private GuiceModelParser() { @@ -172,7 +175,6 @@ private static void indexPrivate(final Map index, fin } } - @SuppressFBWarnings("NP_NULL_PARAM_DEREF") private static ModuleDeclaration initModules(final Map index, final List path) { ModuleDeclaration res = null; @@ -223,12 +225,7 @@ private static void fillDeclaration(final BindingDeclaration dec, final Injector return; } } - Class scope = - (Class) existingBinding.acceptScopingVisitor(SCOPE_DETECTOR); - if (scope != null && scope.equals(EagerSingleton.class)) { - scope = javax.inject.Singleton.class; - } - dec.setScope(scope); + dec.setScope(GuiceModelUtils.getScope(existingBinding)); // important for untargetted bindings to look existing binding if (existingBinding instanceof ConstructorBinding) { final int aops = ((ConstructorBinding) existingBinding).getMethodInterceptors().size(); @@ -249,6 +246,10 @@ private static void fillSource(final BindingDeclaration dec, final Element eleme final Object source = element.getSource(); if (source instanceof Class) { dec.setSource(((Class) source).getName()); + } else if (source instanceof String) { + // possible for synthetic bindings, created by guicey for extensions not directly exposed in + // private modules + dec.setSource((String) source); } else { LOGGER.warn("Unknown element '{}' source: {}", dec, source); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java similarity index 69% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java index 68ab7e07e..2a5b45a3b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/GuiceModelUtils.java @@ -1,5 +1,6 @@ package ru.vyarus.dropwizard.guice.debug.report.guice.util; +import com.google.inject.Binding; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.internal.util.StackTraceElements; @@ -7,9 +8,12 @@ import com.google.inject.spi.ElementSource; import ru.vyarus.dropwizard.guice.debug.report.guice.model.BindingDeclaration; import ru.vyarus.dropwizard.guice.debug.report.guice.model.ModuleDeclaration; +import ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor.GuiceScopingVisitor; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; import ru.vyarus.java.generics.resolver.util.map.EmptyGenericsMap; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.*; import java.util.function.Consumer; @@ -22,6 +26,8 @@ */ public final class GuiceModelUtils { + private static final GuiceScopingVisitor SCOPE_DETECTOR = new GuiceScopingVisitor(); + private GuiceModelUtils() { } @@ -50,8 +56,12 @@ public static Map index(final List m } final Map res = new HashMap<>(); visitBindings(modules, it -> { - if (it.getKey() != null) { - res.put(it.getKey(), it); + final Key key = it.getKey(); + // Duplicate could be in private modules: two bindings with the same key - one is real declaration and + // another is expose declaration. The Original declaration could be a linked declaration, which + // is more important than expose (considering that this index is used for jit and removed detection) + if (key != null && (!res.containsKey(key) || it.getTarget() != null)) { + res.put(key, it); } }); return res; @@ -82,11 +92,24 @@ public static void visitBindings(final List modules, visit(modules, it -> it.getDeclarations().forEach(consumer)); } + /** + * Detects binding scope. + * + * @param binding binding + * @return binding scope + */ + public static Class getScope(final Binding binding) { + Class scope = SCOPE_DETECTOR.performDetection(binding); + if (scope != null && scope.equals(EagerSingleton.class)) { + scope = jakarta.inject.Singleton.class; + } + return scope; + } + /** * @param key guice binding key * @return string representation for key or "-" if key is null */ - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") public static String renderKey(final Key key) { if (key == null) { return "-"; @@ -96,7 +119,7 @@ public static String renderKey(final Key key) { res.append('@').append(key.getAnnotationType().getSimpleName()); for (Method method : key.getAnnotationType().getMethods()) { if ("value".equals(method.getName()) && method.getReturnType().equals(String.class)) { - final boolean accessible = method.isAccessible(); + final boolean accessible = method.canAccess(key.getAnnotation()); try { method.setAccessible(true); final String qualifier = (String) method.invoke(key.getAnnotation()); @@ -136,4 +159,28 @@ public static StackTraceElement getDeclarationSource(final Element element) { } return traceElement; } + + /** + * Render element declaration source. + * + * @param element guice element + * @return element declaration source or null + */ + public static String renderSource(final Element element) { + final StackTraceElement trace = getDeclarationSource(element); + String res = null; + if (trace != null) { + // using full stacktrace element to grant proper link highlight in idea + res = trace.toString(); + } else { + final Object source = element.getSource(); + // source instanceof Class - JIT binding + if (source instanceof String) { + // possible for synthetic bindings, created by guicey for extensions not directly exposed in + // private modules + res = (String) source; + } + } + return res; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java similarity index 75% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java index ad90d52d5..761582089 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/NonObjectMethodMatcher.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.debug.report.guice.util; -import com.google.inject.matcher.AbstractMatcher; +import com.google.inject.matcher.Matcher; import java.lang.reflect.Method; @@ -10,7 +10,7 @@ * @author Vyacheslav Rusakov * @since 23.08.2019 */ -public class NonObjectMethodMatcher extends AbstractMatcher { +public class NonObjectMethodMatcher implements Matcher { @Override public boolean matches(final Method o) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceBindingVisitor.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceBindingVisitor.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceBindingVisitor.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceBindingVisitor.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceElementVisitor.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceElementVisitor.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceElementVisitor.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceElementVisitor.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java new file mode 100644 index 000000000..c4ab0fb64 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java @@ -0,0 +1,100 @@ +package ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor; + +import com.google.inject.Binding; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.Singleton; +import com.google.inject.servlet.RequestScoped; +import com.google.inject.servlet.ServletScopes; +import com.google.inject.servlet.SessionScoped; +import com.google.inject.spi.DefaultBindingScopingVisitor; +import com.google.inject.spi.LinkedKeyBinding; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; +import ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils; +import ru.vyarus.dropwizard.guice.module.support.scope.Prototype; + +import java.lang.annotation.Annotation; + +/** + * Guice binding scope analyzer. Does not support custom scopes. Works correctly only on bindings from injector + * (for module element only manually declared scopes are visible and not annotations). + */ +public class GuiceScopingVisitor + extends DefaultBindingScopingVisitor> { + + /** + * Method to call DIRECTLY on visitor instead of "normal" visitor appliance. Required for more accurate + * scope resolution. + * + * @param binding binding to analyze + * @return resolved scope (or proposed prototype scope when scope not detected) + */ + @SuppressWarnings({"unchecked", "PMD.UnnecessaryCast"}) + public Class performDetection(final Binding binding) { + return finalizeScopeDetection((Class) binding.acceptScopingVisitor(this), binding); + } + + @Override + public Class visitEagerSingleton() { + return EagerSingleton.class; + } + + @Override + public Class visitScope(final Scope scope) { + Class res = null; + if (Scopes.SINGLETON.equals(scope)) { + res = jakarta.inject.Singleton.class; + } + if (Scopes.NO_SCOPE.equals(scope)) { + res = Prototype.class; + } + if (ServletScopes.REQUEST.equals(scope)) { + res = RequestScoped.class; + } + if (ServletScopes.SESSION.equals(scope)) { + res = SessionScoped.class; + } + // not supporting custom scopes + return res; + } + + @Override + @SuppressWarnings("unchecked") + public Class visitScopeAnnotation(final Class scopeAnnotation) { + // always return jakarta.inject annotation to simplify checks + if (scopeAnnotation.equals(Singleton.class)) { + return jakarta.inject.Singleton.class; + } + return scopeAnnotation; + } + + @Override + public Class visitNoScoping() { + // no scope annotation declared OR linked binding with scope annotation on TARGET class (no way to know it) + + return null; + } + + /** + * Method should be called manually! Scoping visitor would not detect scoping annotation on linked binding. + * This method tries to fix this (to improve accuracy). + * + * @param scope resolved scope or null + * @param binding binding under analysis + * @return scoping annotation (exactly resolved or assumed prototype) + */ + private Class finalizeScopeDetection(final Class scope, + final Binding binding) { + if (scope != null) { + return scope; + } + Class res = null; + if (binding instanceof LinkedKeyBinding) { + // NOTE: the link may be a part of long chain, but this is ignored - consider only length of 1 + // (which may obviously produce false scope value) + final LinkedKeyBinding linkedBinding = (LinkedKeyBinding) binding; + res = BindingUtils.findScopingAnnotation(linkedBinding.getLinkedKey().getTypeLiteral().getRawType(), true); + } + return res == null ? Prototype.class : visitScopeAnnotation(res); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java similarity index 74% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java index 3661597d7..073cf85ac 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/PrivateModuleException.java @@ -10,12 +10,23 @@ */ public class PrivateModuleException extends RuntimeException { + /** + * Private elements. + */ private final PrivateElements elements; + /** + * Create exception. + * + * @param elements private elements + */ public PrivateModuleException(final PrivateElements elements) { this.elements = elements; } + /** + * @return private elements + */ public PrivateElements getElements() { return elements; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java index a2614f231..87ee4154c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfig.java @@ -4,10 +4,10 @@ import org.glassfish.jersey.internal.inject.InjectionResolver; import org.glassfish.jersey.server.spi.internal.ValueParamProvider; -import javax.ws.rs.container.ContainerRequestFilter; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.container.DynamicFeature; -import javax.ws.rs.ext.*; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.ext.*; import java.util.LinkedHashSet; import java.util.Set; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java index cd4fd0762..317c3068e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/JerseyConfigRenderer.java @@ -6,8 +6,9 @@ import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding; import ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding; -import javax.annotation.Priority; -import javax.ws.rs.Priorities; +import jakarta.annotation.Priority; +import jakarta.ws.rs.Priorities; + import java.util.Comparator; import java.util.List; @@ -25,6 +26,12 @@ public class JerseyConfigRenderer implements ReportRenderer { private final InjectionManager manager; private final boolean guiceFirstMode; + /** + * Create renderer. + * + * @param manager injection manager + * @param guiceFirstMode true for guice-priority mode (default) + */ public JerseyConfigRenderer(final InjectionManager manager, final boolean guiceFirstMode) { this.manager = manager; this.guiceFirstMode = guiceFirstMode; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java index 97292553f..b273a0c4e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/jersey/util/ProviderRenderUtil.java @@ -2,8 +2,8 @@ import com.google.common.collect.ImmutableMap; import org.glassfish.jersey.internal.inject.InjectionResolver; -import org.glassfish.jersey.jaxb.internal.AbstractCollectionJaxbProvider; import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.ModelProcessor; import org.glassfish.jersey.server.monitoring.ApplicationEventListener; import org.glassfish.jersey.server.spi.internal.ValueParamProvider; import org.glassfish.jersey.spi.ExtendedExceptionMapper; @@ -12,13 +12,13 @@ import ru.vyarus.java.generics.resolver.GenericsResolver; import ru.vyarus.java.generics.resolver.context.GenericsContext; -import javax.ws.rs.Consumes; -import javax.ws.rs.NameBinding; -import javax.ws.rs.Produces; -import javax.ws.rs.container.ContainerRequestFilter; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.container.DynamicFeature; -import javax.ws.rs.ext.*; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.NameBinding; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.ext.*; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; @@ -34,7 +34,6 @@ * @author Vyacheslav Rusakov * @since 26.10.2019 */ -@SuppressWarnings("PMD.CouplingBetweenObjects") public final class ProviderRenderUtil { private static final String SIMPLE_FORMAT = "%s"; private static final String SINGLE_GENERIC_FORMAT = "%-30s %s"; @@ -57,6 +56,7 @@ public final class ProviderRenderUtil { .put(InjectionResolver.class, new ExtDescriptor("Injection resolvers", INJECTION_FORMAT, 1)) .put(ValueParamProvider.class, new ExtDescriptor("Param value providers", SIMPLE_FORMAT, 0)) .put(ApplicationEventListener.class, new ExtDescriptor("Application event listeners", SIMPLE_FORMAT, 0)) + .put(ModelProcessor.class, new ExtDescriptor("Model processors", SIMPLE_FORMAT, 0)) .build(); private ProviderRenderUtil() { @@ -147,7 +147,7 @@ private static String renderUnknown(final Class provider, final boolean isHkM .renderClassLine(provider, collectMarkers(Object.class, provider, isHkManaged, isLazy))); } - @SuppressWarnings({"checkstyle:NPathComplexity", "PMD.NPathComplexity"}) + @SuppressWarnings("checkstyle:NPathComplexity") private static List collectMarkers(final Class ext, final Class provider, final boolean isHkManaged, @@ -169,7 +169,6 @@ private static List collectMarkers(final Class ext, return markers; } - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") private static String renderLine(final Class ext, final Class provider, final ExtDescriptor desc, @@ -183,12 +182,24 @@ private static String renderLine(final Class ext, } params[pos] = RenderUtils.renderClassLine(provider, collectMarkers(ext, provider, isHkManaged, isLazy)); // special case for message body readers and writers to identify collection mappers - if ("Object".equals(params[0]) && AbstractCollectionJaxbProvider.class.isAssignableFrom(provider)) { + if ("Object".equals(params[0]) && isAbstractCollectionJaxbProvider(provider)) { params[0] = "T[], Collection"; } return String.format(desc.format, params); } + // AbstractCollectionJaxbProvider located inside (org.glassfish.jersey.media:jersey-media-jaxb:2.36) + // artifact, not present by default (but it is a transitive dependency for dropwizard-testing), + // and so can't be checked with isAssignableFrom + private static boolean isAbstractCollectionJaxbProvider(final Class provider) { + return (MessageBodyReader.class.isAssignableFrom(provider) + || MessageBodyWriter.class.isAssignableFrom(provider)) + && GenericsResolver.resolve(provider).getGenericsInfo() + .getComposingTypes().stream() + .map(Class::getName) + .anyMatch("org.glassfish.jersey.jaxb.internal.AbstractCollectionJaxbProvider"::equals); + } + private static String renderMessageReaderWriter(final Class ext, final Class provider, final boolean isHkManaged, @@ -242,9 +253,9 @@ private static String renderInjectionResolver(final InjectionResolver instance, */ @SuppressWarnings("checkstyle:VisibilityModifier") private static class ExtDescriptor { - public String name; - public String format; - public int generics; + public final String name; + public final String format; + public final int generics; ExtDescriptor(final String name, final String format, final int generics) { this.name = name; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java index ec5a69b02..318dc8da6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsConfig.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.debug.report.option; -import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -83,7 +83,7 @@ public OptionsConfig showNotDefinedOptions() { */ @SafeVarargs public final OptionsConfig hideGroups(final Class... groups) { - hiddenGroups.addAll(Arrays.asList(groups)); + Collections.addAll(hiddenGroups, groups); return this; } @@ -94,7 +94,7 @@ public final OptionsConfig hideGroups(final Class... groups) { * @return config instance for chained calls */ public OptionsConfig hideOptions(final Enum... options) { - hidden.addAll(Arrays.asList(options)); + Collections.addAll(hidden, options); return this; } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java index 062991c50..c9c6033e6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/option/OptionsRenderer.java @@ -36,6 +36,11 @@ public class OptionsRenderer implements ReportRenderer { private final GuiceyConfigurationInfo info; + /** + * Create renderer. + * + * @param info guicey info + */ public OptionsRenderer(final GuiceyConfigurationInfo info) { this.info = info; } @@ -103,6 +108,7 @@ private boolean isHidden(final Enum option, final OptionsConfig config) { || (!config.isShowNotDefinedOptions() && !info.getOptions().isSet(option)); } + @SuppressWarnings("unchecked") private String valueToString(final Object value) { final String res; if (value == null) { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/DropwizardBundlesTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/DropwizardBundlesTracker.java new file mode 100644 index 000000000..f3212a8c3 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/DropwizardBundlesTracker.java @@ -0,0 +1,173 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import com.google.common.base.Stopwatch; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsInfo; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Dropwizard bundles tracker for startup time report. Replaces bundles list inside + * {@link io.dropwizard.core.setup.Bootstrap} object to detect bundles addition and wrap them to track + * run phase. Note that bundle added to collection AFTER initialization call. Also, bundle may add other bundles, + * which initialization would be called immediately and so it is impossible to measure initialization time + * of each bundle. So, instead of measuring bundle init time, bundle init done point is registered (it should + * be enough to highlight slow bundles). + *

+ * It is not possible to measure init time for bundles, registered before the guice bundle. Warning would be shown. + * + * @author Vyacheslav Rusakov + * @since 09.03.2025 + */ +@SuppressFBWarnings({"CT_CONSTRUCTOR_THROW", "EQ_DOESNT_OVERRIDE_EQUALS", "SE_BAD_FIELD"}) +public class DropwizardBundlesTracker extends ArrayList { + /** + * Logger. + */ + private final Logger logger = LoggerFactory.getLogger(DropwizardBundlesTracker.class); + + /** + * Stats. + */ + private final StatsInfo stats; + /** + * Startup info. + */ + private final StartupTimeInfo info; + /** + * OverallTime stat would finish with lifecycle listener event slightly earlier than listener in startup + * diagnostic (because it will be registered later). Use custom timer for more accurate time measuring. + */ + private final Stopwatch webTimer = Stopwatch.createUnstarted(); + + // start timer since guice bundle startup - the earliest point we can track + + /** + * Create bundles tracker. + * + * @param stats stat objects + * @param info startup time data + * @param bootstrap bootstrap instance + */ + public DropwizardBundlesTracker(final StatsInfo stats, final StartupTimeInfo info, final Bootstrap bootstrap) { + this.stats = stats; + this.info = info; + injectTracker(bootstrap); + } + + /** + * @return web time tracker + */ + public Stopwatch getWebTimer() { + return webTimer; + } + + @Override + public boolean add(final ConfiguredBundle configuredBundle) { + // intercept bundle addition - at this moment bundle initialization already done, so we will eventually know + // bundles initialization done point + // Wrap with tracker to know the point of bundles run done (there may be other bundles registered after guice) + super.add(new BundleRunTracker(configuredBundle)); + // register time since initialization start! Because bundles can init other bundles we can't know exact + // bundle init time, but can show time point when init was done! + info.getBundlesInitPoints().put(configuredBundle.getClass(), stats.duration(Stat.OverallTime)); + // last init done - init phase completed + info.setInitTime(stats.duration(Stat.OverallTime)); + info.setInitInstallersTime(stats.duration(Stat.InstallersTime)); + info.setInitExtensionsTime(stats.duration(Stat.ExtensionsRecognitionTime)); + info.setInitListenersTime(stats.duration(Stat.ListenersTime)); + + if (configuredBundle instanceof GuiceBundle) { + info.getInitEvents().addAll(stats.getDetailedStats(DetailStat.Listener).keySet()); + } + return true; + } + + @Override + public void add(final int index, final ConfiguredBundle element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final int index, final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public ConfiguredBundle set(final int index, final ConfiguredBundle element) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + private void injectTracker(final Bootstrap bootstrap) { + try { + final Field configuredBundles = Bootstrap.class.getDeclaredField("configuredBundles"); + configuredBundles.setAccessible(true); + final List existing = (List) configuredBundles.get(bootstrap); + if (!existing.isEmpty()) { + logger.warn("Initialization time not tracked for bundles (move them after guice bundle to " + + "measure time): {}", existing.stream() + .map(configuredBundle -> RenderUtils.getClassName(configuredBundle.getClass())) + .collect(Collectors.joining(", "))); + // pack with tracker (for run phase) + existing.forEach(this::add); + } + configuredBundles.set(bootstrap, this); + } catch (Exception e) { + throw new IllegalStateException("Failed to inject bootstrap bundles tracker", e); + } + } + + /** + * Wrapper object (delegate) for registered dropwizard bundles to be able to measure bundle run time. + */ + private class BundleRunTracker implements ConfiguredBundle { + + private final ConfiguredBundle bundle; + + BundleRunTracker(final ConfiguredBundle bundle) { + this.bundle = bundle; + } + + @Override + @SuppressWarnings("unchecked") + public void run(final Configuration configuration, final Environment environment) throws Exception { + if (info.getDwPreRunTime() == null) { + // time between last dw bundle init and first run (config and environment creation) + info.setDwPreRunTime(stats.duration(Stat.OverallTime).minus(info.getInitTime())); + } + final Stopwatch bundleTimer = Stopwatch.createStarted(); + bundle.run(configuration, environment); + info.getBundlesRunTimes().put(bundle.getClass(), bundleTimer.stop().elapsed()); + // last bundle run end - end of run phase (application run can't be tracked) + info.setRunPoint(stats.duration(Stat.OverallTime)); + info.setRunListenersTime(stats.duration(Stat.ListenersTime).minus(info.getInitListenersTime())); + // reset because it would start after each dropwizard bundle run + webTimer.reset().start(); + + if (bundle instanceof GuiceBundle) { + info.getRunEvents().addAll(stats.getDetailedStats(DetailStat.Listener).keySet()); + info.getRunEvents().removeAll(info.getInitEvents()); + } + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ManagedTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ManagedTracker.java new file mode 100644 index 000000000..38b39a37d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ManagedTracker.java @@ -0,0 +1,163 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import com.google.common.base.Stopwatch; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.dropwizard.lifecycle.JettyManaged; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import org.eclipse.jetty.util.component.LifeCycle; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventListener; +import java.util.List; + +/** + * Managed objects tracker for startup time report. Replaces managed objects list inside + * {@link io.dropwizard.lifecycle.setup.LifecycleEnvironment} to wrap existing and future managed (and lifecycle) + * objects (to be able to track start and stop executions). + * + * @author Vyacheslav Rusakov + * @since 10.03.2025 + */ +@SuppressFBWarnings({"CT_CONSTRUCTOR_THROW", "EQ_DOESNT_OVERRIDE_EQUALS", "SE_BAD_FIELD"}) +public class ManagedTracker extends ArrayList { + + /** + * Startup time. + */ + private final StartupTimeInfo start; + /** + * Shutdown time. + */ + private final ShutdownTimeInfo stop; + + /** + * Create tracker. + * + * @param start start info + * @param stop stop info + * @param lifecycle lifecycle instance + */ + public ManagedTracker(final StartupTimeInfo start, + final ShutdownTimeInfo stop, + final LifecycleEnvironment lifecycle) { + this.start = start; + this.stop = stop; + injectTracker(lifecycle); + } + + @Override + public boolean add(final LifeCycle lifeCycle) { + // wraps managed or lifecycle object to track its execution + return super.add(new LifeCycleTracker(lifeCycle)); + } + + @Override + public void add(final int index, final LifeCycle element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final int index, final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public LifeCycle set(final int index, final LifeCycle element) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + private void injectTracker(final LifecycleEnvironment lifecycle) { + try { + final Field managedObjects = LifecycleEnvironment.class.getDeclaredField("managedObjects"); + managedObjects.setAccessible(true); + final List existing = (List) managedObjects.get(lifecycle); + if (!existing.isEmpty()) { + // pack with tracker (to measure start/stop execution) + existing.forEach(this::add); + } + managedObjects.set(lifecycle, this); + } catch (Exception e) { + throw new IllegalStateException("Failed to inject managed objects tracker", e); + } + } + + /** + * Wrapper for dropwizard lifecycle objects (managed). Used to record start/stop execution times. + */ + private class LifeCycleTracker implements LifeCycle { + + private final LifeCycle object; + private final boolean managed; + private final Class type; + + LifeCycleTracker(final LifeCycle object) { + this.object = object; + managed = object instanceof JettyManaged; + type = managed ? ((JettyManaged) object).getManaged().getClass() : object.getClass(); + } + + @Override + public void start() throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + object.start(); + start.getManagedTimes().put(type, timer.stop().elapsed()); + start.getManagedTypes().put(type, managed ? "managed" : "lifecycle"); + } + + @Override + public void stop() throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + object.stop(); + stop.getManagedTimes().put(type, timer.stop().elapsed()); + stop.getManagedTypes().put(type, managed ? "managed" : "lifecycle"); + } + + @Override + public boolean isRunning() { + return object.isRunning(); + } + + @Override + public boolean isStarted() { + return object.isStarted(); + } + + @Override + public boolean isStarting() { + return object.isStarting(); + } + + @Override + public boolean isStopping() { + return object.isStopping(); + } + + @Override + public boolean isStopped() { + return object.isStopped(); + } + + @Override + public boolean isFailed() { + return object.isFailed(); + } + + @Override + public boolean addEventListener(final EventListener listener) { + return object.addEventListener(listener); + } + + @Override + public boolean removeEventListener(final EventListener listener) { + return object.removeEventListener(listener); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeInfo.java new file mode 100644 index 000000000..127298402 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeInfo.java @@ -0,0 +1,89 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import ru.vyarus.dropwizard.guice.module.context.stat.StatsInfo; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Shutdown time aggregation object (for startup report). + * + * @author Vyacheslav Rusakov + * @since 10.03.2025 + */ +public class ShutdownTimeInfo { + + private Duration stopTime; + private final Map managedTimes = new LinkedHashMap<>(); + // managed or lifecycle + private final Map managedTypes = new LinkedHashMap<>(); + private Duration listenersTime; + private final List events = new ArrayList<>(); + private StatsInfo stats; + + /** + * @return shutdown time + */ + public Duration getStopTime() { + return stopTime; + } + + /** + * @return managed objects durations + */ + public Map getManagedTimes() { + return managedTimes; + } + + /** + * @return types of managed objects + */ + public Map getManagedTypes() { + return managedTypes; + } + + /** + * @return overall listeners time + */ + public Duration getListenersTime() { + return listenersTime; + } + + /** + * @param listenersTime overall listeners time + */ + public void setListenersTime(final Duration listenersTime) { + this.listenersTime = listenersTime; + } + + /** + * @param stopTime shutdown time + */ + public void setStopTime(final Duration stopTime) { + this.stopTime = stopTime; + } + + /** + * @return executed guicey events + */ + public List getEvents() { + return events; + } + + /** + * @return stats instance + */ + public StatsInfo getStats() { + return stats; + } + + /** + * @param stats stats instance + */ + public void setStats(final StatsInfo stats) { + this.stats = stats; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeRenderer.java new file mode 100644 index 000000000..3d190b726 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/ShutdownTimeRenderer.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import com.google.common.base.MoreObjects; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.time.Duration; + +/** + * Render shutdown time report. Could only show stopping-stop lifecycle time and managed objects stop method call. + * + * @author Vyacheslav Rusakov + * @since 10.03.2025 + */ +public class ShutdownTimeRenderer { + + /** + * Render shutdown report. + * + * @param info shutdown times + * @return rendered report + */ + public String render(final ShutdownTimeInfo info) { + final StringBuilder res = new StringBuilder(200); + res.append("\n\n").append(line(1, "Application shutdown", info.getStopTime())); + + info.getManagedTimes().forEach((type, duration) -> { + res.append(tab(2)).append(String.format("%-10s", info.getManagedTypes().get(type))) + .append(line(0, RenderUtils.getClassName(type), duration)); + }); + + res.append(line(2, "Listeners time", info.getListenersTime())); + info.getStats().getDetailedStats(DetailStat.Listener).forEach((type, time) -> { + if (info.getEvents().contains(type)) { + res.append(line(3, type.getSimpleName(), time)); + } + }); + + return res.toString(); + } + + private String line(final int shift, + final String name, + final Duration duration) { + return line(shift, name, null, duration, null); + } + + private String line(final int shift, + final String name, + final String prefix, + final Duration duration, + final String postfix) { + return tab(shift) + format(name, prefix, duration, postfix); + } + + private String format(final String name, final String prefix, final Duration duration, final String postfix) { + return String.format("%-35s: %s%s%s%n", name, MoreObjects.firstNonNull(prefix, ""), + PrintUtils.ms(duration), MoreObjects.firstNonNull(postfix, "")); + } + + private String tab(final int shift) { + final StringBuilder res = new StringBuilder(); + for (int i = 0; i < shift; i++) { + res.append('\t'); + } + return res.toString(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeInfo.java new file mode 100644 index 000000000..77deb6840 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeInfo.java @@ -0,0 +1,296 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import org.eclipse.jetty.util.Uptime; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsInfo; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Startup time aggregation object (for startup report). There are 3 main phases: init - everything from start till + * last dw bundle init, run - dw bundles run (configuration and environment creation tracked separately as time + * between bundles init and run), web - everything after bundles run until jersey lifecycle start (also includes + * application run method). + * + * @author Vyacheslav Rusakov + * @since 07.03.2025 + */ +public class StartupTimeInfo { + + // jvm time before application start + private final long jvmStart = Uptime.getUptime(); + + // from guice bundle creation until last dropwizard bundle init (not include app init method) + private Duration initTime; + // last dropwizard bundle run finished (time since app start); does not include app run method) + private Duration runPoint; + // exclusive web lifecycle start time + private Duration webTime; + + // time since start for each dropwizard bundle (can't be counted exclusively) + private final Map, Duration> bundlesInitPoints = new LinkedHashMap<>(); + private final Multimap, Class> guiceyBundleTransitives = ArrayListMultimap.create(); + private List> guiceyBundlesInitOrder; + // exclusive run time for each bundle + private final Map, Duration> bundlesRunTimes = new LinkedHashMap<>(); + + private Duration initListenersTime; + private Duration runListenersTime; + + private Duration initInstallersTime; + private Duration initExtensionsTime; + // configuration and environment creation time + private Duration dwPreRunTime; + + // pure jersey time + private Duration jerseyTime; + // time between lifecycle starting and start + private Duration lifecycleTime; + // have to separate - otherwise can't differentiate them + private final Map managedTimes = new LinkedHashMap<>(); + // managed or lifecycle + private final Map managedTypes = new LinkedHashMap<>(); + + private final List initEvents = new ArrayList<>(); + private final List runEvents = new ArrayList<>(); + private final List webEvents = new ArrayList<>(); + + private StatsInfo stats; + + /** + * @return jvm time before application + */ + public long getJvmStart() { + return jvmStart; + } + + /** + * @return from guice bundle creation until last dropwizard bundle init (not include app init method) + */ + public Duration getInitTime() { + return initTime; + } + + /** + * @param initTime overall init time + */ + public void setInitTime(final Duration initTime) { + this.initTime = initTime; + } + + /** + * @return guice bundles hierarchy + */ + public Multimap, Class> getGuiceyBundleTransitives() { + return guiceyBundleTransitives; + } + + /** + * @return guicey bundles in initialization order + */ + public List> getGuiceyBundlesInitOrder() { + return guiceyBundlesInitOrder; + } + + /** + * @param guiceyBundlesInitOrder guicey bundles in initialization order + */ + public void setGuiceyBundlesInitOrder(final List> guiceyBundlesInitOrder) { + this.guiceyBundlesInitOrder = guiceyBundlesInitOrder; + } + + /** + * @return last dropwizard bundle run finished (time since app start); does not include app run method) + */ + public Duration getRunPoint() { + return runPoint; + } + + /** + * @param runPoint last dropwizard bundle run finished + */ + public void setRunPoint(final Duration runPoint) { + this.runPoint = runPoint; + } + + /** + * @return exclusive web lifecycle start time + */ + public Duration getWebTime() { + return webTime; + } + + /** + * @param webTime exclusive web lifecycle start time + */ + public void setWebTime(final Duration webTime) { + this.webTime = webTime; + } + + /** + * @return time since start for each dropwizard bundle (can't be counted exclusively) + */ + public Map, Duration> getBundlesInitPoints() { + return bundlesInitPoints; + } + + /** + * @return bundles run times + */ + public Map, Duration> getBundlesRunTimes() { + return bundlesRunTimes; + } + + /** + * @return overall listeners time during initialization + */ + public Duration getInitListenersTime() { + return initListenersTime; + } + + /** + * @param initListenersTime overall listeners time during initialization + */ + public void setInitListenersTime(final Duration initListenersTime) { + this.initListenersTime = initListenersTime; + } + + /** + * @return overall listeners time during run + */ + public Duration getRunListenersTime() { + return runListenersTime; + } + + /** + * @param runListenersTime overall listeners time during run + */ + public void setRunListenersTime(final Duration runListenersTime) { + this.runListenersTime = runListenersTime; + } + + /** + * @return overall extensions init time + */ + public Duration getInitExtensionsTime() { + return initExtensionsTime; + } + + /** + * @param initExtensionsTime overall extensions init time + */ + public void setInitExtensionsTime(final Duration initExtensionsTime) { + this.initExtensionsTime = initExtensionsTime; + } + + /** + * @return overall installers init time + */ + public Duration getInitInstallersTime() { + return initInstallersTime; + } + + /** + * @param initInstallersTime overall installers init time + */ + public void setInitInstallersTime(final Duration initInstallersTime) { + this.initInstallersTime = initInstallersTime; + } + + /** + * @return time between last dw bundle init and first run (config and environment creation) + */ + public Duration getDwPreRunTime() { + return dwPreRunTime; + } + + /** + * @param dwPreRunTime time between last dw bundle init and first run (config and environment creation) + */ + public void setDwPreRunTime(final Duration dwPreRunTime) { + this.dwPreRunTime = dwPreRunTime; + } + + /** + * @return jersey time + */ + public Duration getJerseyTime() { + return jerseyTime; + } + + /** + * @param jerseyTime jersey time + */ + public void setJerseyTime(final Duration jerseyTime) { + this.jerseyTime = jerseyTime; + } + + /** + * @return jersey startup time + */ + public Duration getLifecycleTime() { + return lifecycleTime; + } + + /** + * @param lifecycleTime jersey startup time + */ + public void setLifecycleTime(final Duration lifecycleTime) { + this.lifecycleTime = lifecycleTime; + } + + /** + * @return managed objects startup times + */ + public Map getManagedTimes() { + return managedTimes; + } + + /** + * @return types of managed objects + */ + public Map getManagedTypes() { + return managedTypes; + } + + /** + * @return guicey events executed during initialization + */ + public List getInitEvents() { + return initEvents; + } + + /** + * @return guicey events executed during run + */ + public List getRunEvents() { + return runEvents; + } + + /** + * @return guicey events executed during web start + */ + public List getWebEvents() { + return webEvents; + } + + /** + * @return stats instance + */ + public StatsInfo getStats() { + return stats; + } + + /** + * @param stats stats instance + */ + public void setStats(final StatsInfo stats) { + this.stats = stats; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeRenderer.java new file mode 100644 index 000000000..e904e2030 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/start/StartupTimeRenderer.java @@ -0,0 +1,214 @@ +package ru.vyarus.dropwizard.guice.debug.report.start; + +import com.google.common.base.MoreObjects; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.time.Duration; +import java.util.Map; + +/** + * Render startup times. + * + * @author Vyacheslav Rusakov + * @since 07.03.2025 + */ +@SuppressWarnings("MultipleStringLiterals") +public class StartupTimeRenderer { + + /** + * Render startup time report. + * + * @param info startup info + * @return rendered report + */ + public String render(final StartupTimeInfo info) { + final StringBuilder res = new StringBuilder(200); + res.append("\n\n").append(line(1, "JVM time before", Duration.ofMillis(info.getJvmStart()))) + + .append('\n') + .append(line(1, "Application startup", info.getStats().duration(Stat.OverallTime))) + + .append(line(2, "Dropwizard initialization", info.getInitTime())); + info.getBundlesInitPoints().forEach((s, point) -> { + if (GuiceBundle.class.equals(s)) { + printGuiceyInit(3, point, info, res); + } else { + res.append(line(3, RenderUtils.getClassName(s), "finished since start at ", point, null)); + } + }); + + res.append('\n').append(line(2, "Dropwizard run", info.getRunPoint().minus(info.getInitTime()))) + .append(line(3, "Configuration and Environment", info.getDwPreRunTime())); + info.getBundlesRunTimes().forEach((s, duration) -> { + if (GuiceBundle.class.equals(s)) { + printGuiceyRun(3, duration, info, res); + } else { + res.append(line(3, RenderUtils.getClassName(s), duration)); + } + }); + + res.append('\n').append(line(2, "Web server startup", info.getWebTime())); + printGuiceyWeb(3, info, res); + + return res.toString(); + } + + private void printGuiceyInit(final int shift, + final Duration point, + final StartupTimeInfo info, + final StringBuilder res) { + res.append(line(shift, GuiceBundle.class.getSimpleName(), null, + // exclude dropwizard bundles time (registered through guicey) - time tracked separately + info.getStats().duration(Stat.ConfigurationTime) + .minus(info.getStats().duration(Stat.DropwizardBundleInitTime)), + " (finished since start at " + PrintUtils.ms(point) + ")")) + + .append(line(shift + 1, "Bundle builder time", + info.getStats().duration(Stat.BundleBuilderTime))) + + .append(line(shift + 1, "Hooks processing", + info.getStats().duration(Stat.HooksTime))); + + info.getStats().getDetailedStats(DetailStat.Hook).forEach((type, duration) -> { + final String className = RenderUtils.getClassName(type); + res.append(line(shift + 2, className, duration)); + }); + + res.append(line(shift + 1, "Classpath scan", info.getStats().duration(Stat.ScanTime))) + + .append(line(shift + 1, "Commands processing", info.getStats().duration(Stat.CommandTime))); + info.getStats().getDetailedStats(DetailStat.Command).forEach((type, duration) -> + res.append(line(shift + 2, RenderUtils.getClassName(type), duration))); + + res.append(line(shift + 1, "Bundles lookup", info.getStats().duration(Stat.BundleResolutionTime))) + + .append(line(shift + 1, "Guicey bundles init", info.getStats().duration(Stat.GuiceyBundleInitTime))); + final Map, Duration> detailedStats = info.getStats().getDetailedStats(DetailStat.BundleInit); + // bundle stats would contain incorrect init order + info.getGuiceyBundlesInitOrder().forEach(type -> { + Duration actual = detailedStats.get(type); + // exclude transitive bundles time + for (Class transitive : info.getGuiceyBundleTransitives().get(type)) { + actual = actual.minus(detailedStats.get(transitive)); + } + res.append(line(shift + 2, RenderUtils.getClassName(type), actual)); + }); + + res.append(line(shift + 1, "Installers time", info.getInitInstallersTime())) + .append(line(shift + 2, "Installers resolution", + info.getStats().duration(Stat.InstallersResolutionTime))) + .append(line(shift + 2, "Scanned extensions recognition", info.getInitExtensionsTime())) + + .append(line(shift + 1, "Listeners time", info.getInitListenersTime())); + info.getStats().getDetailedStats(DetailStat.Listener).forEach((type, time) -> { + if (info.getInitEvents().contains(type)) { + res.append(line(shift + 2, type.getSimpleName(), time)); + } + }); + } + + private void printGuiceyRun(final int shift, + final Duration duration, + final StartupTimeInfo info, + final StringBuilder res) { + // same as info.getStats().duration(Stat.RunTime) but slightly more accurate + res.append(line(shift, GuiceBundle.class.getSimpleName(), duration)) + .append(line(shift + 1, "Configuration analysis", info.getStats().duration(Stat.ConfigurationAnalysis))) + .append(line(shift + 1, "Guicey bundles run", info.getStats().duration(Stat.GuiceyBundleRunTime))); + + // here order would be correct because there is no transitive bundles installation + info.getStats().getDetailedStats(DetailStat.BundleRun).forEach((type, time) -> + res.append(line(shift + 2, RenderUtils.getClassName(type), time))); + + final Duration bindingsAnalysisTime = info.getStats().duration(Stat.BindingsAnalysisTime); + res.append(line(shift + 1, "Guice modules processing", info.getStats().duration(Stat.ModulesProcessingTime))) + .append(line(shift + 2, "Bindings resolution", info.getStats().duration(Stat.BindingsResolutionTime))) + + .append(line(shift + 1, "Installers time", info.getStats().duration(Stat.InstallersTime) + .minus(info.getInitInstallersTime()) + .plus(info.getStats().duration(Stat.ExtensionsInstallationTime)))) + .append(line(shift + 2, "Extensions registration", info.getStats() + .duration(Stat.ExtensionsRecognitionTime) + .minus(info.getInitExtensionsTime()) + .minus(bindingsAnalysisTime))) + .append(line(shift + 2, "Guice bindings analysis", bindingsAnalysisTime)) + .append(line(shift + 2, "Extensions installation", info.getStats() + .duration(Stat.ExtensionsInstallationTime))) + + .append(line(shift + 1, "Injector creation", info.getStats().duration(Stat.InjectorCreationTime))); + info.getStats().getGuiceStats().forEach(s -> { + if (!s.endsWith(" 0 ms")) { + final String[] val = s.split(": "); + res.append(tab(shift + 2)).append(String.format("%-35s: %s%n", val[0], val[1])); + } + }); + + res.append(line(shift + 1, "Listeners time", info.getRunListenersTime())); + info.getStats().getDetailedStats(DetailStat.Listener).forEach((type, time) -> { + if (info.getRunEvents().contains(type)) { + res.append(line(shift + 2, type.getSimpleName(), time)); + } + }); + } + + private void printGuiceyWeb(final int shift, + final StartupTimeInfo info, + final StringBuilder res) { + + final boolean lifecycleSimulation = info.getJerseyTime() == null; + res.append(line(shift, lifecycleSimulation ? "Lifecycle simulation time" + : "Jetty lifecycle time", info.getLifecycleTime())); + + info.getManagedTimes().forEach((type, duration) -> + res.append(tab(shift + 1)).append(String.format("%-10s", info.getManagedTypes().get(type))) + .append(line(0, RenderUtils.getClassName(type), duration))); + + final int prefix = lifecycleSimulation ? shift : shift + 1; + if (!lifecycleSimulation) { + res.append(line(prefix, "Jersey time", info.getJerseyTime())); + } + + final Duration listenersTime = info.getStats().duration(Stat.ListenersTime) + .minus(info.getRunListenersTime()).minus(info.getInitListenersTime()); + res.append(line(prefix + 1, "Guicey time", info.getStats().duration(Stat.JerseyTime).plus(listenersTime))) + .append(line(prefix + 2, "Installers time", info.getStats().duration(Stat.JerseyInstallerTime))) + + .append(line(prefix + 2, "Listeners time", listenersTime)); + info.getStats().getDetailedStats(DetailStat.Listener).forEach((type, time) -> { + if (info.getWebEvents().contains(type)) { + res.append(line(prefix + 3, type.getSimpleName(), time)); + } + }); + } + + private String line(final int shift, + final String name, + final Duration duration) { + return line(shift, name, null, duration, null); + } + + private String line(final int shift, + final String name, + final String prefix, + final Duration duration, + final String postfix) { + return tab(shift) + format(name, prefix, duration, postfix); + } + + private String format(final String name, final String prefix, final Duration duration, final String postfix) { + return String.format("%-35s: %s%s%s%n", name, MoreObjects.firstNonNull(prefix, ""), + PrintUtils.ms(duration), MoreObjects.firstNonNull(postfix, "")); + } + + private String tab(final int shift) { + final StringBuilder res = new StringBuilder(); + for (int i = 0; i < shift; i++) { + res.append('\t'); + } + return res.toString(); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java similarity index 99% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java index 6d590d7e5..bcfa1cb71 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/stat/StatsRenderer.java @@ -24,6 +24,11 @@ public class StatsRenderer implements ReportRenderer { private final GuiceyConfigurationInfo info; + /** + * Create renderer. + * + * @param info guicey info + */ public StatsRenderer(final GuiceyConfigurationInfo info) { this.info = info; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java index b9e6d3317..b8718faad 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeConfig.java @@ -3,7 +3,8 @@ import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; -import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Set; @@ -24,7 +25,7 @@ */ public final class ContextTreeConfig { - private final Set items = new HashSet<>(); + private final Set items = EnumSet.noneOf(ConfigItem.class); private final Set> scopes = new HashSet<>(); private boolean disables; private boolean notUsedInstallers; @@ -127,7 +128,7 @@ public ContextTreeConfig hideCommands() { * @see ItemInfo#getRegisteredBy() for more info about scopes */ public ContextTreeConfig hideScopes(final Class... avoid) { - scopes.addAll(Arrays.asList(avoid)); + Collections.addAll(scopes, avoid); return this; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java similarity index 98% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java index 7dc2031b3..0ff852bef 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/tree/ContextTreeRenderer.java @@ -71,6 +71,11 @@ public class ContextTreeRenderer implements ReportRenderer { private final GuiceyConfigurationInfo service; + /** + * Create configuration tree renderer. + * + * @param service configuration info service + */ public ContextTreeRenderer(final GuiceyConfigurationInfo service) { this.service = service; } @@ -122,11 +127,10 @@ private void renderSpecialScope(final ContextTreeConfig config, final Set scopes, final TreeNode root) { - if (config.getHiddenScopes().contains(ConfigScope.Module.getType()) + if (config.getHiddenScopes().contains(Module.getType()) || config.getHiddenItems().contains(ConfigItem.Module)) { return; } @@ -232,6 +236,7 @@ private void renderItem(final TreeNode root, * @param markers markers (may be null) */ @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_INFERRED") + @SuppressWarnings("PMD.LiteralsFirstInComparisons") private void renderLeaf(final TreeNode root, final String name, final ItemId item, final int pos, final List markers) { final boolean ignored = markers != null diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/MappingsConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/MappingsConfig.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/MappingsConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/MappingsConfig.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java index a1f1f8f79..5edd3e1e3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/WebMappingsRenderer.java @@ -11,11 +11,11 @@ import com.google.inject.spi.Elements; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.dropwizard.jetty.MutableServletContextHandler; -import io.dropwizard.setup.Environment; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.FilterMapping; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; +import io.dropwizard.core.setup.Environment; +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.FilterMapping; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletMapping; import ru.vyarus.dropwizard.guice.debug.report.ReportRenderer; import ru.vyarus.dropwizard.guice.debug.report.guice.util.GuiceModelUtils; import ru.vyarus.dropwizard.guice.debug.report.web.model.WebElementModel; @@ -51,6 +51,12 @@ public class WebMappingsRenderer implements ReportRenderer { private final Environment environment; private final List modules; + /** + * Create renderer. + * + * @param environment environment + * @param info guicey info + */ public WebMappingsRenderer(final Environment environment, final GuiceyConfigurationInfo info) { this.environment = environment; @@ -103,7 +109,6 @@ private void renderContext(final MappingsConfig config, } } - @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") private Multimap renderContextFilters(final MappingsConfig config, final MutableServletContextHandler handler, final TreeNode root) throws Exception { @@ -111,7 +116,7 @@ private Multimap renderContextFilters(final MappingsCon for (FilterMapping mapping : handler.getServletHandler().getFilterMappings()) { final FilterHolder holder = handler.getServletHandler().getFilter(mapping.getFilterName()); // single filter instance used for both contexts and so the name is also the same - final boolean isGuiceFilter = mapping.getFilterName().equals(GuiceWebModule.GUICE_FILTER); + final boolean isGuiceFilter = GuiceWebModule.GUICE_FILTER.equals(mapping.getFilterName()); if ((isGuiceFilter && !config.isGuiceMappings()) || !isAllowed(holder.getClassName(), config)) { continue; @@ -131,7 +136,6 @@ private Multimap renderContextFilters(final MappingsCon return servletFilters; } - @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_INFERRED") private void renderServlet(final ServletMapping mapping, final ServletHolder holder, final Multimap servletFilters, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java similarity index 69% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java index b50a3c653..6c5f0042a 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementModel.java @@ -14,11 +14,17 @@ public class WebElementModel { private final WebElementType type; private final Key key; private final boolean instance; - + private String pattern; private UriPatternType patternType; - + /** + * Create model. + * + * @param type element type + * @param key binding key + * @param instance true for instance + */ public WebElementModel(final WebElementType type, final Key key, final boolean instance) { this.type = type; @@ -26,30 +32,51 @@ public WebElementModel(final WebElementType type, this.instance = instance; } + /** + * @param pattern binding pattern + */ public void setPattern(final String pattern) { this.pattern = pattern; } + /** + * @param patternType binding pattern type + */ public void setPatternType(final UriPatternType patternType) { this.patternType = patternType; } + /** + * @return element type + */ public WebElementType getType() { return type; } + /** + * @return binding pattern + */ public String getPattern() { return pattern; } + /** + * @return binding pattern type + */ public UriPatternType getPatternType() { return patternType; } + /** + * @return binding key + */ public Key getKey() { return key; } + /** + * @return true for binding by instance + */ public boolean isInstance() { return instance; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java similarity index 66% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java index 8546bc037..539612296 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/model/WebElementType.java @@ -7,5 +7,12 @@ * @since 23.10.2019 */ public enum WebElementType { - SERVLET, FILTER + /** + * HTTP servlet. + */ + SERVLET, + /** + * HTTP filter. + */ + FILTER } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/util/ServletVisitor.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/util/ServletVisitor.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/util/ServletVisitor.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/web/util/ServletVisitor.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java index c57141853..4e2329d7f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/BindingsConfig.java @@ -45,7 +45,8 @@ public BindingsConfig showNullValues() { } /** - * Avoid paths from dropwizard {@link io.dropwizard.Configuration} class (only custom configuration paths shown). + * Avoid paths from dropwizard {@link io.dropwizard.core.Configuration} class (only custom configuration paths + * shown). * * @return config object for chained calls */ diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java similarity index 74% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java index 853d1f113..1907a688c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/report/yaml/ConfigBindingsRenderer.java @@ -1,11 +1,18 @@ package ru.vyarus.dropwizard.guice.debug.report.yaml; -import io.dropwizard.Configuration; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Key; +import io.dropwizard.core.Configuration; import ru.vyarus.dropwizard.guice.debug.report.ReportRenderer; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.debug.util.TreeNode; import ru.vyarus.dropwizard.guice.module.yaml.ConfigPath; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; +import java.lang.annotation.Annotation; +import java.util.Collection; + import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.NEWLINE; import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.TAB; @@ -31,6 +38,11 @@ public class ConfigBindingsRenderer implements ReportRenderer { private final ConfigurationTree tree; + /** + * Create configuration renderer. + * + * @param tree parsed configuration + */ public ConfigBindingsRenderer(final ConfigurationTree tree) { this.tree = tree; } @@ -44,6 +56,7 @@ public String renderReport(final BindingsConfig config) { if (config.isShowBindings()) { renderRootTypes(config, res); renderUniqueSubConfigs(config, res); + renderQualified(res); renderPaths(config, res); } return res.toString(); @@ -109,6 +122,46 @@ private void renderUniqueSubConfigs(final BindingsConfig config, final StringBui } } + private void renderQualified(final StringBuilder res) { + final Multimap, ConfigPath> bindings = LinkedHashMultimap.create(); + for (ConfigPath item : tree.getPaths()) { + if (item.getQualifier() != null) { + final Key key = Key.get(item.getDeclaredTypeWithGenerics(), item.getQualifier()); + bindings.put(key, item); + } + } + + boolean header = false; + for (Key key : bindings.keySet()) { + final Collection values = bindings.get(key); + final ConfigPath first = values.iterator().next(); + final Annotation qualifier = first.getQualifier(); + if (qualifier == null) { + continue; + } + if (!header) { + // delayed render for no displayed items case + res.append(NEWLINE).append(NEWLINE).append(TAB) + .append("Qualified bindings:").append(NEWLINE); + header = true; + } + + res.append(TAB).append(TAB).append(RenderUtils.renderAnnotation(qualifier)).append(' '); + + if (values.size() > 1) { + res.append("Set<").append(first.toStringDeclaredType()).append("> = (aggregated values)\n"); + for (ConfigPath path : values) { + res.append(TAB).append(TAB).append(TAB); + renderPath(path, res); + res.append(" (").append(path.getPath()).append(')').append(NEWLINE); + } + } else { + renderPath(first, res); + res.append(" (").append(first.getPath()).append(')').append(NEWLINE); + } + } + } + private void renderPaths(final BindingsConfig config, final StringBuilder res) { boolean header = false; Class rootConfig = null; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java index bf8f03a60..38266bfca 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviator.java @@ -19,10 +19,21 @@ public class ClassNameAbbreviator { private final int targetLength; + /** + * Create abbreviator. + * + * @param targetLength maximum length + */ public ClassNameAbbreviator(final int targetLength) { this.targetLength = targetLength; } + /** + * Shorten package names to match max length. + * + * @param fqClassName class name + * @return abbreviated name + */ public String abbreviate(final String fqClassName) { if (fqClassName == null) { throw new IllegalArgumentException("Class name may not be null"); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java similarity index 80% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java index 4648fe766..43e8e230c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/RenderUtils.java @@ -1,9 +1,12 @@ package ru.vyarus.dropwizard.guice.debug.util; import com.google.common.base.Joiner; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.List; /** @@ -169,6 +172,39 @@ public static String getClassName(final Class type) { return name; } + /** + * The same as {@link #getClassName(Class)} but for inner classes would preserve upper classes. + * For example, would print {@code SomeClass$Inner} instead of just {@code Inner} for inner class. + * + * @param type type to get class name from + * @return full class name (including root classes for inner class declarations) + */ + public static String getFullClassName(final Class type) { + return type.getName().substring(type.getName().lastIndexOf('.') + 1); + } + + /** + * Render annotation. Supports only "value" annotation method - other possible methods simply ignored. + * + * @param annotation annotation to render + * @return rendered annotation string + */ + @SuppressFBWarnings("DE_MIGHT_IGNORE") + public static String renderAnnotation(final Annotation annotation) { + final StringBuilder res = new StringBuilder("@").append(annotation.annotationType().getSimpleName()); + // NOTE custom config annotations might contain custom values - it can't be known for sure + try { + final Method valueMethod = FeatureUtils.findMethod(annotation.annotationType(), "value"); + final Object value = FeatureUtils.invokeMethod(valueMethod, annotation); + if (value != null) { + res.append("(\"").append(value).append("\")"); + } + } catch (Exception ignored) { + // no value field in annotation + } + return res.toString(); + } + private static String renderPositionPostfix(final int pos) { return pos > 1 ? "#" + pos : ""; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/util/TreeNode.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/TreeNode.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/debug/util/TreeNode.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/debug/util/TreeNode.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java index 74de311c6..066e7cb0b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/ConfigurationHooksSupport.java @@ -1,12 +1,21 @@ package ru.vyarus.dropwizard.guice.hook; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.util.PropertyUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Extra configuration mechanism to apply external configuration to bundle builder after manual configuration in @@ -114,16 +123,26 @@ public static void reset() { * Could be used in tests to disable configuration items and (probably) replace them. * * @param builder just created builder + * @param stat stats tracker * @return used hooks */ - public static Set run(final GuiceBundle.Builder builder) { + public static Set run(final GuiceBundle.Builder builder, final StatsTracker stat) { final Set hooks = HOOKS.get(); if (hooks != null) { - hooks.forEach(l -> l.configure(builder)); + hooks.forEach(l -> { + final Stopwatch timer = stat.detailTimer(DetailStat.Hook, l.getClass()); + try { + l.configure(builder); + } catch (Exception ex) { + Throwables.throwIfUnchecked(ex); + throw new IllegalStateException("Failed to run hook", ex); + } + timer.stop(); + }); } // clear hooks just after init reset(); - return hooks; + return hooks == null ? Collections.emptySet() : hooks; } /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java index 270ec5cd6..2bd7885c7 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/hook/GuiceyConfigurationHook.java @@ -43,16 +43,17 @@ public interface GuiceyConfigurationHook { *

  • Guice bindings override: * {@link GuiceBundle.Builder#modulesOverride(com.google.inject.Module...)}
  • * - * All other configuration options are also available, so it is possible to register extra extensions, bundles etc + * All other configuration options are also available, so it is possible to register extra extensions, bundles etc. * or modify guicey options ({@link GuiceBundle.Builder#option(Enum, Object)}). *

    * All configuration items, registered with hook will be scoped as {@link GuiceyConfigurationHook} - * instead of {@link io.dropwizard.Application} and so will be clearly distinguishable in configuration logs + * instead of {@link io.dropwizard.core.Application} and so will be clearly distinguishable in configuration logs * ({@link GuiceBundle.Builder#printDiagnosticInfo()}). * * @param builder just created bundle's builder + * @throws java.lang.Exception on error (simplify usage) */ - void configure(GuiceBundle.Builder builder); + void configure(GuiceBundle.Builder builder) throws Exception; /** * Register hook. Note that it must be called before guicey bundle creation, otherwise will never be called. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/injector/DefaultInjectorFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/DefaultInjectorFactory.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/injector/DefaultInjectorFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/DefaultInjectorFactory.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java index aeef620ac..ae2c32a05 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/InjectorFactory.java @@ -10,6 +10,7 @@ * @author Nicholas Pace * @since Dec 26, 2014 */ +@FunctionalInterface public interface InjectorFactory { /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java index dc12aa22a..aa128a49c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorLookup.java @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.injector.lookup; import com.google.inject.Injector; -import io.dropwizard.Application; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import java.util.Optional; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java index 1b3a96133..eadef8c57 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/injector/lookup/InjectorProvider.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.injector.lookup; import com.google.inject.Injector; -import io.dropwizard.Application; +import io.dropwizard.core.Application; -import javax.inject.Provider; +import jakarta.inject.Provider; /** * Lazy injector provider. Used internally instead of direct injector reference when injector is not constructed yet. @@ -19,6 +19,11 @@ public class InjectorProvider implements Provider { private final Application application; private Injector injector; + /** + * Create provider. + * + * @param application application instance + */ public InjectorProvider(final Application application) { this.application = application; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java index 93b9f7e89..69f25e949 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceBootstrapModule.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.module; import com.google.inject.Scopes; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.ConfigurationInfo; import ru.vyarus.dropwizard.guice.module.context.option.Options; @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.support.scope.Prototype; import ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * Bootstrap integration guice module. @@ -42,6 +42,11 @@ public class GuiceBootstrapModule extends DropwizardAwa private final ConfigurationContext context; + /** + * Create bootstrap module. + * + * @param context configuration context + */ public GuiceBootstrapModule(final ConfigurationContext context) { this.context = context; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java index f39770286..f0cbf1afd 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyConfigurationInfo.java @@ -2,13 +2,14 @@ import com.google.common.collect.Sets; import com.google.inject.Module; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.cli.Command; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.cli.Command; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigScope; import ru.vyarus.dropwizard.guice.module.context.ConfigurationInfo; import ru.vyarus.dropwizard.guice.module.context.Filters; import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.GuiceyBundleItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.ModuleItemInfo; @@ -20,10 +21,13 @@ import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; -import javax.inject.Inject; +import jakarta.inject.Inject; + import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly; @@ -45,6 +49,15 @@ public class GuiceyConfigurationInfo { private final ExtensionsHolder holder; private final ConfigurationTree configurationTree; + /** + * Create configuration info. + * + * @param context configuration context + * @param stats stat trackers + * @param options options + * @param holder extensions holder + * @param configurationTree parsed configuration + */ @Inject public GuiceyConfigurationInfo(final ConfigurationInfo context, final StatsInfo stats, final OptionsInfo options, final ExtensionsHolder holder, @@ -206,6 +219,9 @@ public List> getCommands() { /** * Note that multiple instances could be installed for some bundles, but returned list will contain just * bundle type (no matter how many instances were actually installed). + *

    + * Important: bundles returned in the registration order; if initialization order is required see + * {@link #getGuiceyBundlesInInitOrder()} (different because of transitive bundles). * * @return types of all installed and enabled bundles (including lookup bundles) or empty list */ @@ -213,6 +229,25 @@ public List> getGuiceyBundles() { return typesOnly(getGuiceyBundleIds()); } + /** + * Guicey bundle could register another guicey bundle then this transitive bundle would + * be initialized before the root bundle. This method sorts registered bundles according to + * actual initialization order (not by when init started, but when init finished - + * the same order as for transitive dropwizard bundles). + * + * @return applied guicey bundles in initialization order. + */ + @SuppressWarnings("unchecked") + public List> getGuiceyBundlesInInitOrder() { + final List infos = context.getInfos(ConfigItem.Bundle, Filters.enabled()); + return infos.stream() + // sort by initialization time to move transitive bundles up + // (by default, bundles in the registration order) + .sorted(Comparator.comparingInt(GuiceyBundleItemInfo::getInitOrder)) + .map(info -> (Class) info.getType()) + .collect(Collectors.toList()); + } + /** * Note that this list could be larger then {@link #getGuiceyBundles()} because multiple bundle instances * of the same class could be registered. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java similarity index 68% rename from src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java index 8a91c194c..4a65826ba 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyInitializer.java @@ -1,37 +1,44 @@ package ru.vyarus.dropwizard.guice.module; import com.google.common.base.Preconditions; -import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import io.dropwizard.cli.Command; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.setup.Bootstrap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup; -import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; -import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; import ru.vyarus.dropwizard.guice.module.installer.CoreInstallersBundle; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.internal.CommandSupport; import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; -import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsSupport; import ru.vyarus.dropwizard.guice.module.installer.option.WithOptions; import ru.vyarus.dropwizard.guice.module.installer.order.OrderComparator; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClassVisitor; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import ru.vyarus.dropwizard.guice.module.installer.util.BundleSupport; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; -import static ru.vyarus.dropwizard.guice.GuiceyOptions.*; -import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.*; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.ScanPackages; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.ScanProtectedClasses; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.SearchCommands; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.UseCoreInstallers; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.BundleResolutionTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.BundleTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ConfigurationTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ExtensionsRecognitionTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.GuiceyBundleInitTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.GuiceyTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersResolutionTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersTime; /** * Guicey initialization logic performed under dropwizard configuration phase. @@ -51,19 +58,24 @@ */ @SuppressWarnings("PMD.ExcessiveImports") public class GuiceyInitializer { + /** + * Special package name for classpath scan configuration to use application location package. + */ + public static final String APP_PKG = ""; private static final OrderComparator COMPARATOR = new OrderComparator(); private final Logger logger = LoggerFactory.getLogger(GuiceyInitializer.class); - private final Stopwatch guiceyTimer; - private final Stopwatch confTimer; - private final Bootstrap bootstrap; private final ConfigurationContext context; private final ClasspathScanner scanner; + /** + * Create initializer. + * + * @param bootstrap bootstrap + * @param context configuration context + */ public GuiceyInitializer(final Bootstrap bootstrap, final ConfigurationContext context) { - guiceyTimer = context.stat().timer(GuiceyTime); - confTimer = context.stat().timer(ConfigurationTime); // this will also trigger registered dropwizard bundles initialization // (so dropwizard bundles init before guicey bundles) @@ -72,9 +84,15 @@ public GuiceyInitializer(final Bootstrap bootstrap, final ConfigurationContext c this.bootstrap = bootstrap; this.context = context; final String[] packages = context.option(ScanPackages); + final boolean acceptProtected = context.option(ScanProtectedClasses); + // configuration shortcut for all packages starting from application location + if (packages.length == 1 && APP_PKG.equals(packages[0])) { + packages[0] = bootstrap.getApplication().getClass().getPackage().getName(); + } // classpath scan performed immediately (if required) this.scanner = packages.length > 0 - ? new ClasspathScanner(Sets.newHashSet(Arrays.asList(packages)), context.stat()) : null; + ? new ClasspathScanner( + Sets.newHashSet(Arrays.asList(packages)), acceptProtected, context.stat()) : null; } /** @@ -83,14 +101,14 @@ public GuiceyInitializer(final Bootstrap bootstrap, final ConfigurationContext c * @param bundleLookup bundle lookup object */ public void initializeBundles(final GuiceyBundleLookup bundleLookup) { - final Stopwatch timer = context.stat().timer(BundleTime); - final Stopwatch resolutionTimer = context.stat().timer(BundleResolutionTime); + final StatTimer timer = context.stat().timer(BundleTime); + final StatTimer resolutionTimer = context.stat().timer(BundleResolutionTime); if (context.option(UseCoreInstallers)) { context.registerBundles(new CoreInstallersBundle()); } context.registerLookupBundles(bundleLookup.lookup()); resolutionTimer.stop(); - final Stopwatch btime = context.stat().timer(GuiceyBundleInitTime); + final StatTimer btime = context.stat().timer(GuiceyBundleInitTime); BundleSupport.initBundles(context); btime.stop(); timer.stop(); @@ -114,45 +132,32 @@ public void findCommands() { * Perform classpath scan to find installers. Create enabled installer instances. */ public void resolveInstallers() { - final Stopwatch timer = context.stat().timer(InstallersTime); + final StatTimer itimer = context.stat().timer(InstallersTime); + final StatTimer timer = context.stat().timer(InstallersResolutionTime); final List> installerClasses = findInstallers(); final List installers = prepareInstallers(installerClasses); - context.installersResolved(installers); timer.stop(); + itimer.stop(); + context.installersResolved(installers); } /** - * Performs classpath scan to search for extensions. Register all extensions (note that extensions may be disabled - * on run phase). + * Performs classpath scan to search for extensions. No registration performed because manual extensions could + * be added in run phase (and it is important to register manual extension first). */ - @SuppressWarnings("PMD.PrematureDeclaration") - public void resolveExtensions() { - final Stopwatch itimer = context.stat().timer(InstallersTime); - final Stopwatch timer = context.stat().timer(ExtensionsRecognitionTime); + public void scanExtensions() { + final StatTimer itimer = context.stat().timer(InstallersTime); + final StatTimer timer = context.stat().timer(ExtensionsRecognitionTime); final ExtensionsHolder holder = context.getExtensionsHolder(); - final List> manual = context.getEnabledExtensions(); - for (Class type : manual) { - if (!ExtensionsSupport.registerExtension(context, type, false)) { - throw new IllegalStateException("No installer found for extension " + type.getName() - + ". Available installers: " + holder.getInstallerTypes() - .stream().map(FeatureUtils::getInstallerExtName).collect(Collectors.joining(", "))); - } - } - context.lifecycle().manualExtensionsValidated(context.getItems(ConfigItem.Extension), manual); if (scanner != null) { final List> extensions = new ArrayList<>(); scanner.scan(type -> { - if (manual.contains(type)) { - // avoid duplicate extension installation, but register it's appearance in auto scan scope - context.getOrRegisterExtension(type, true); + // detect by installer - if installer found for sure it is an extension + if (context.isAcceptableAutoScanClass(type) && holder.acceptScanCandidate(type)) { extensions.add(type); - } else { - // if matching installer found - extension recognized, otherwise - not an extension - if (ExtensionsSupport.registerExtension(context, type, true)) { - extensions.add(type); - } } }); + // fire event with detected extensions, but they are not registered yet context.lifecycle().classpathExtensionsResolved(extensions); } timer.stop(); @@ -168,8 +173,8 @@ public void initFinished() { } context.lifecycle().initialized(); - confTimer.stop(); - guiceyTimer.stop(); + context.stat().stopTimer(ConfigurationTime); + context.stat().stopTimer(GuiceyTime); } /** @@ -210,14 +215,12 @@ public void visit(final Class type) { private List prepareInstallers( final List> installerClasses) { final List installers = Lists.newArrayList(); - // different instance then used in guice context, but it's just an accessor object - final Options options = new Options(context.options()); for (Class installerClass : installerClasses) { try { - final FeatureInstaller installer = installerClass.newInstance(); + final FeatureInstaller installer = InstanceUtils.create(installerClass); installers.add(installer); if (WithOptions.class.isAssignableFrom(installerClass)) { - ((WithOptions) installer).setOptions(options); + ((WithOptions) installer).setOptions(context.optionsReadOnly()); } } catch (Exception e) { throw new IllegalStateException("Failed to register installer " + installerClass.getName(), e); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java similarity index 56% rename from src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java index ebc47508a..cb8611b31 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/GuiceyRunner.java @@ -1,23 +1,36 @@ package ru.vyarus.dropwizard.guice.module; -import com.google.common.base.Stopwatch; import com.google.inject.Injector; import com.google.inject.Module; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.injector.InjectorFactory; import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; import ru.vyarus.dropwizard.guice.module.installer.internal.CommandSupport; +import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsSupport; import ru.vyarus.dropwizard.guice.module.installer.internal.ModulesSupport; import ru.vyarus.dropwizard.guice.module.installer.util.BundleSupport; +import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; import static ru.vyarus.dropwizard.guice.GuiceyOptions.InjectorStage; -import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.*; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.BundleTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.CommandTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ExtensionsInstallationTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ExtensionsRecognitionTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.GuiceyBundleRunTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.GuiceyTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InjectorCreationTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ModulesProcessingTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.RunTime; /** * Guicey run logic performed under dropwizard run phase. @@ -27,18 +40,21 @@ */ public class GuiceyRunner { - private final Stopwatch guiceyTime; - private final Stopwatch runTime; - private final ConfigurationContext context; - private Injector injector; + /** + * Create runner. + * + * @param context configuration context + * @param configuration configuration + * @param environment environment + */ public GuiceyRunner(final ConfigurationContext context, final Configuration configuration, final Environment environment) { - guiceyTime = context.stat().timer(GuiceyTime); - runTime = context.stat().timer(RunTime); + context.stat().timer(GuiceyTime); + context.stat().timer(RunTime); context.runPhaseStarted(configuration, environment); this.context = context; @@ -53,18 +69,53 @@ public GuiceyRunner(final ConfigurationContext context, * @throws Exception if something goes wrong */ public void runBundles() throws Exception { - final Stopwatch timer = context.stat().timer(BundleTime); + final StatTimer timer = context.stat().timer(BundleTime); + final StatTimer runTimer = context.stat().timer(GuiceyBundleRunTime); BundleSupport.runBundles(context); + runTimer.stop(); timer.stop(); } + /** + * Extensions could be registered in run phase too. Auto scan was performed under configuration phase, but without + * registration (only recognition) - now register all extensions. + */ + public void registerExtensions() { + context.stat().timer(InstallersTime); + context.stat().timer(ExtensionsRecognitionTime); + final ExtensionsHolder holder = context.getExtensionsHolder(); + final List> manual = context.getEnabledExtensions(); + for (Class type : manual) { + if (!ExtensionsSupport.registerExtension(context, type, false)) { + throw new IllegalStateException("No installer found for extension " + type.getName() + + ". Available installers: " + holder.getInstallerTypes() + .stream().map(FeatureUtils::getInstallerExtName).collect(Collectors.joining(", "))); + } + } + context.lifecycle().manualExtensionsValidated(context.getItems(ConfigItem.Extension), manual); + // install pre-selected classpath scan extensions + if (holder.getScanExtensions() != null) { + holder.getScanExtensions().forEach(cand -> { + final Class type = cand.getType(); + if (manual.contains(type)) { + // avoid duplicate extension installation, but register it's appearance in auto scan scope + context.getOrRegisterExtension(type, true); + } else { + ExtensionsSupport.registerExtension(context, type, cand.getInstaller(), true); + } + }); + } + context.stat().stopTimer(ExtensionsRecognitionTime); + context.stat().stopTimer(InstallersTime); + } + /** * Prepare guice modules for injector creation. */ public void prepareModules() { - final Stopwatch timer = context.stat().timer(ModulesProcessingTime); + final StatTimer timer = context.stat().timer(ModulesProcessingTime); // dropwizard specific bindings and jersey integration - context.registerModules(new GuiceBootstrapModule(context)); + context.registerModules(new GuiceBootstrapModule<>(context)); ModulesSupport.configureModules(context); timer.stop(); } @@ -98,7 +149,7 @@ public Iterable analyzeAndRepackageBindings() { * @param modules guice modules to use for injector */ public void createInjector(final InjectorFactory injectorFactory, final Iterable modules) { - final Stopwatch timer = context.stat().timer(InjectorCreationTime); + final StatTimer timer = context.stat().timer(InjectorCreationTime); context.lifecycle().injectorCreation( new ArrayList<>(context.getNormalModules()), new ArrayList<>(context.getOverridingModules()), @@ -117,19 +168,26 @@ public void createInjector(final InjectorFactory injectorFactory, final Iterable * Execute extensions installation (by type and instance). */ public void installExtensions() { - final Stopwatch timer = context.stat().timer(ExtensionsInstallationTime); + final StatTimer timer = context.stat().timer(ExtensionsInstallationTime); ExtensionsSupport.installExtensions(context, injector); timer.stop(); } + /** + * Inject application fields (to use in run method). + */ + public void injectApplication() { + injector.injectMembers(context.getBootstrap().getApplication()); + } + /** * Inject fields in registered commands. This step is actually required only if currently executed dropwizard * command require such injections. */ @SuppressWarnings("unchecked") public void injectCommands() { - final Stopwatch timer = context.stat().timer(CommandTime); - CommandSupport.initCommands(context.getBootstrap().getCommands(), injector); + final StatTimer timer = context.stat().timer(CommandTime); + CommandSupport.initCommands(context.getBootstrap().getCommands(), injector, context.stat()); timer.stop(); } @@ -139,7 +197,7 @@ public void injectCommands() { public void runFinished() { context.bundleStarted(); - runTime.stop(); - guiceyTime.stop(); + context.stat().stopTimer(RunTime); + context.stat().stopTimer(GuiceyTime); } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java index 98c8bc8e6..6a9720d77 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigItem.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.context; -import io.dropwizard.ConfiguredBundle; +import io.dropwizard.core.ConfiguredBundle; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.impl.*; @@ -22,13 +22,13 @@ public enum ConfigItem { */ Extension(false), /** - * {@link io.dropwizard.ConfiguredBundle}. Only bundles registered through guicey api are tracked. + * {@link io.dropwizard.core.ConfiguredBundle}. Only bundles registered through guicey api are tracked. */ // NOTE dropwizard bundle goes before guicey bundle because dropwizard bundles are actually register first DropwizardBundle(true), /** * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle} or - * {@link io.dropwizard.ConfiguredBundle} + * {@link io.dropwizard.core.ConfiguredBundle} * Note that guicey bundle installs other items and all of them are tracked too. */ Bundle(true), @@ -43,7 +43,7 @@ public enum ConfigItem { */ Command(false); - private boolean instanceConfig; + private final boolean instanceConfig; ConfigItem(final boolean instanceConfig) { this.instanceConfig = instanceConfig; @@ -63,7 +63,7 @@ public boolean isInstanceConfig() { * @param type of required info container * @return info container instance */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "PMD.ExhaustiveSwitchHasDefault"}) public T newContainer(final Object item) { final ItemInfo res; switch (this) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java index 00ae30040..05666c9d4 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigScope.java @@ -22,7 +22,7 @@ public enum ConfigScope { /** * Application scope: everything registered directly in guice bundle's builder. */ - Application(io.dropwizard.Application.class), + Application(io.dropwizard.core.Application.class), /** * Lookup scope contains all bundles, resolved with lookup mechanism. */ @@ -46,7 +46,7 @@ public enum ConfigScope { * It was added just for completeness of context recognition logic (see {@link #recognize(Class)}) * and to indicate all possible scopes. */ - DropwizardBundle(io.dropwizard.ConfiguredBundle.class), + DropwizardBundle(io.dropwizard.core.ConfiguredBundle.class), /** * WARNING: binding extension scope is guice module name itself (not direct module, but topmost registered * module - visible in configuration). It was added just for completeness of context recognition logic diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java index 6e4c9ec34..e263e5efc 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationContext.java @@ -2,15 +2,17 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; -import com.google.common.base.Stopwatch; -import com.google.common.collect.*; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.google.inject.Module; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import io.dropwizard.Configuration; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.cli.Command; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.GuiceBundle; @@ -20,8 +22,14 @@ import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.module.context.bootstrap.BootstrapProxyFactory; import ru.vyarus.dropwizard.guice.module.context.bootstrap.DropwizardBundleTracker; -import ru.vyarus.dropwizard.guice.module.context.info.*; +import ru.vyarus.dropwizard.guice.module.context.info.DropwizardBundleItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.GuiceyBundleItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.InstanceItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ItemId; +import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ModuleItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.impl.ExtensionItemInfoImpl; +import ru.vyarus.dropwizard.guice.module.context.info.impl.GuiceyBundleItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.info.impl.InstanceItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.info.impl.ItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.info.impl.ModuleItemInfoImpl; @@ -29,17 +37,29 @@ import ru.vyarus.dropwizard.guice.module.context.option.Option; import ru.vyarus.dropwizard.guice.module.context.option.Options; import ru.vyarus.dropwizard.guice.module.context.option.internal.OptionsSupport; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.context.unique.DuplicateConfigDetector; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.LifecycleSupport; import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -64,11 +84,13 @@ * @since 06.07.2016 */ @SuppressWarnings({"PMD.GodClass", "PMD.TooManyMethods", "checkstyle:ClassFanOutComplexity", - "PMD.ExcessiveImports", "PMD.ExcessivePublicCount", "PMD.NcssCount", "PMD.CyclomaticComplexity", - "PMD.TooManyFields", "PMD.ClassDataAbstractionCoupling", "checkstyle:ClassDataAbstractionCoupling"}) + "PMD.ExcessiveImports", "PMD.ExcessivePublicCount", "PMD.CyclomaticComplexity", + "checkstyle:ClassDataAbstractionCoupling", "PMD.CouplingBetweenObjects"}) public final class ConfigurationContext { private final Logger logger = LoggerFactory.getLogger(ConfigurationContext.class); + private final List>> autoScanFilters = new ArrayList<>(); + private final List delayedConfigurations = new ArrayList<>(); private final SharedConfigurationState sharedState = new SharedConfigurationState(); private DuplicateConfigDetector duplicates; private Bootstrap bootstrap; @@ -77,7 +99,12 @@ public final class ConfigurationContext { private ConfigurationTree configurationTree; private Environment environment; private ExtensionsHolder extensionsHolder; + private List initOrder; + /** + * Record executed hook classes. + */ + private final List> hookTypes = new ArrayList<>(); /** * Configured items (bundles, installers, extensions etc). @@ -126,12 +153,43 @@ public final class ConfigurationContext { /** * Used to set and get options within guicey. */ - private final OptionsSupport optionsSupport = new OptionsSupport(); + private final OptionsSupport optionsSupport = new OptionsSupport<>(); + private final Options readOnlyOptions = new Options(optionsSupport); /** * Guicey lifecycle listeners support. */ - private final LifecycleSupport lifecycleTracker = new LifecycleSupport(new Options(optionsSupport), sharedState); + private final LifecycleSupport lifecycleTracker = new LifecycleSupport(tracker, readOnlyOptions, + sharedState, tracker::verifyTimersDone); + + /** + * Create a context. + */ + public ConfigurationContext() { + // always available + sharedState.put(Options.class, readOnlyOptions); + } + + /** + * Add extra filter for scanned classes (classpath scan and bindings recognition). + * + * @param filter filter instance + */ + public void addAutoScanFilter(final Predicate> filter) { + autoScanFilters.add(filter); + } + /** + * @param clazz class to test + * @return true if extension class could be used, false otherwise (auto scan filter) + */ + public boolean isAcceptableAutoScanClass(final Class clazz) { + for (Predicate> filter : autoScanFilters) { + if (!filter.test(clazz)) { + return false; + } + } + return true; + } /** * Change default duplicates detector. @@ -146,6 +204,26 @@ public void setDuplicatesDetector(final DuplicateConfigDetector detector) { this.duplicates = detector; } + /** + * Add delayed configuration from the main builder (or hook) to run under run phase (when configuration would be + * available). + * + * @param config delayed configuration + */ + public void addDelayedConfiguration(final Consumer config) { + delayedConfigurations.add(new DelayedConfig(getScope(), config)); + } + + /** + * Process delayed builder (or hooks) configurations. + * + * @param environment guicey environment + */ + public void processDelayedConfigurations(final GuiceyEnvironment environment) { + for (DelayedConfig action : delayedConfigurations) { + action.process(environment); + } + } // --------------------------------------------------------------------------- SCOPE @@ -221,7 +299,6 @@ public void registerLookupBundles(final List bundles) { * @param bundles bundles to register * @return list of actually registered bundles (without duplicates) */ - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") public List registerBundles(final GuiceyBundle... bundles) { final List res = new ArrayList<>(); for (GuiceyBundle bundle : bundles) { @@ -254,6 +331,32 @@ public List getEnabledBundles() { return getEnabledItems(ConfigItem.Bundle); } + /** + * Store initialization order of bundles. Due to transitive bundles immediate installation, real initialization + * order could differ from registration order (because the root bundle would be registered before + * a transitive bundle, but its processing would be finished after it). + * + * @param orderedBundles bundles in initialization order + */ + public void storeBundlesInitOrder(final List orderedBundles) { + initOrder = orderedBundles; + int order = 1; + for (GuiceyBundle bundle : orderedBundles) { + ((GuiceyBundleItemInfoImpl) getInfo(bundle)).setInitOrder(order++); + } + } + + /** + * It is important to call bundles run in the same order as bundles were initialized (it would + * differ from registration order due to transitive bundles). + * + * @return all bundles in the initialization order + */ + public List getBundlesOrdered() { + return initOrder; + } + + /** * Note: before configuration finalization this returns all actually disabled bundles and after * finalization all disables (including never registered bundles). @@ -286,14 +389,13 @@ public boolean isBundleEnabled(final ItemId id) { * * @param bundles dropwizard bundles */ - @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") public void registerDropwizardBundles(final ConfiguredBundle... bundles) { for (ConfiguredBundle bundle : bundles) { final DropwizardBundleItemInfo info = register(ConfigItem.DropwizardBundle, bundle); // register only non duplicate bundles // bundles, registered in root GuiceBundle will be registered as soon as bootstrap would be available if (info.getRegistrationAttempts() == 1 && bootstrap != null) { - registerDropwizardBundle(bundle); + installDropwizardBundle(bundle); } } } @@ -569,6 +671,17 @@ public ExtensionItemInfoImpl getOrRegisterBindingExtension(final Class extens return info; } + /** + * Extension recognized by installer, which means it is now completely initialized and disable predicates + * could be applied now. + * + * @param extension extension after setting installer + */ + public void notifyExtensionRecognized(final ExtensionItemInfoImpl extension) { + Preconditions.checkState(extension.isAllDataCollected()); + fireRegistration(extension, true); + } + /** * Extension manual disable registration from * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableExtensions(Class[])}. @@ -637,6 +750,13 @@ public OptionsSupport options() { return optionsSupport; } + /** + * @return read-only options + */ + public Options optionsReadOnly() { + return readOnlyOptions; + } + // --------------------------------------------------------------------------- GENERAL /** @@ -647,10 +767,12 @@ public OptionsSupport options() { * * @param predicates disable predicates */ - @SuppressWarnings("PMD.UseVarargs") - public void registerDisablePredicates(final Predicate[] predicates) { + @SuppressWarnings({"PMD.UseVarargs", "unchecked"}) + public void registerDisablePredicates(final Predicate[] predicates) { + // accept typed predicates, but downgrade to base entity (assumed that predicate would be formed + // correctly with Disables.* methods) final List list = Arrays.stream(predicates) - .map(p -> new PredicateHandler(p, getScope())) + .map(p -> new PredicateHandler((Predicate) p, getScope())) .collect(Collectors.toList()); disablePredicates.addAll(list); applyPredicatesForRegisteredItems(list); @@ -662,6 +784,7 @@ public void registerDisablePredicates(final Predicate[] predicates) { * @param builder bundle builder */ public void runHooks(final GuiceBundle.Builder builder) { + final StatTimer timer = stat().timer(Stat.HooksTime); ConfigurationHooksSupport.logRegisteredAliases(); // lookup hooks from system property "guicey.hooks" (lookup executed after builder configuration to // let user declare alias hooks) @@ -669,8 +792,10 @@ public void runHooks(final GuiceBundle.Builder builder) { // Support for external configuration (for tests) // Use special scope to distinguish external configuration openScope(ConfigScope.Hook.getKey()); - final Set hooks = ConfigurationHooksSupport.run(builder); + final Set hooks = ConfigurationHooksSupport.run(builder, stat()); + hooks.forEach(hook -> hookTypes.add(hook.getClass())); closeScope(); + timer.stop(); lifecycle().configurationHooksProcessed(hooks); } @@ -682,16 +807,17 @@ public void initPhaseStarted(final Bootstrap bootstrap) { // register in shared state just in case this.sharedState.put(Bootstrap.class, bootstrap); this.sharedState.assignTo(bootstrap.getApplication()); + lifecycle().beforeInit(bootstrap); // delayed init of registered dropwizard bundles - final Stopwatch time = stat().timer(BundleTime); - final Stopwatch dwtime = stat().timer(DropwizardBundleInitTime); + final StatTimer time = stat().timer(BundleTime); + final StatTimer dwtime = stat().timer(DropwizardBundleInitTime); for (ConfiguredBundle bundle : getEnabledDropwizardBundles()) { - registerDropwizardBundle(bundle); + installDropwizardBundle(bundle); } - lifecycle().initializationStarted(bootstrap, getEnabledDropwizardBundles(), - getDisabledDropwizardBundles(), getIgnoredItems(ConfigItem.DropwizardBundle)); dwtime.stop(); time.stop(); + lifecycle().dropwizardBundlesInitialized(getEnabledDropwizardBundles(), + getDisabledDropwizardBundles(), getIgnoredItems(ConfigItem.DropwizardBundle)); } /** @@ -700,8 +826,10 @@ public void initPhaseStarted(final Bootstrap bootstrap) { */ public void runPhaseStarted(final Configuration configuration, final Environment environment) { this.configuration = configuration; + final StatTimer timer = stat().timer(Stat.ConfigurationAnalysis); this.configurationTree = ConfigTreeBuilder .build(bootstrap, configuration, option(BindConfigurationByPath)); + timer.stop(); this.environment = environment; // register in shared state just in case this.sharedState.put(Configuration.class, configuration); @@ -851,6 +979,13 @@ public SharedConfigurationState getSharedState() { return sharedState; } + /** + * @return types of executed hooks + */ + public List> getExecutedHookTypes() { + return hookTypes; + } + private ItemId getScope() { return currentScope == null ? ConfigScope.Application.getKey() : currentScope; } @@ -870,6 +1005,8 @@ private void applyPredicatesForRegisteredItems(final List pred .build() .stream() .map(this::getInfo) + // avoid extensions without installer set (could be important for disable) + .filter(ItemInfo::isAllDataCollected) .forEach(item -> applyDisablePredicates(predicates, item)); } @@ -905,7 +1042,7 @@ private T register(final ConfigItem type, final Object // if registered multiple times in one scope attempts will reveal it info.countRegistrationAttempt(getScope()); - fireRegistration(info); + fireRegistration(info, false); return info; } @@ -928,7 +1065,6 @@ private T register(final ConfigItem type, final Object * @param item item instance * @return correct item to use for registration */ - @SuppressFBWarnings("NP_NULL_PARAM_DEREF") private Object detectDuplicate(final ConfigItem type, final Object item) { Object original = null; if (type.isInstanceConfig()) { @@ -1026,9 +1162,11 @@ type, getScope().getType().getSimpleName(), ItemId.from(item), return original; } - private void fireRegistration(final ItemInfo item) { - // fire event only for initial registration and for items which could be disabled - if (item instanceof DisableSupport && item.getRegistrationAttempts() == 1) { + private void fireRegistration(final ItemInfo item, final boolean afterCompleteInitialization) { + // Fire event only for initial registration and for items which could be disabled + // Apply predicate ONLY when all required data prepared (affects extensions) + if (item instanceof DisableSupport && item.isAllDataCollected() + && (item.getRegistrationAttempts() == 1 || afterCompleteInitialization)) { applyDisablePredicates(disablePredicates, item); } } @@ -1085,11 +1223,11 @@ private boolean isEnabled(final ConfigItem type, final ItemId itemId) { } @SuppressWarnings("unchecked") - private void registerDropwizardBundle(final ConfiguredBundle bundle) { + private void installDropwizardBundle(final ConfiguredBundle bundle) { // register decorated dropwizard bundle to track transitive bundles // or bundle directly if tracking disabled bootstrap.addBundle(option(GuiceyOptions.TrackDropwizardBundles) - ? new DropwizardBundleTracker(bundle, this) : bundle); + ? new DropwizardBundleTracker<>(bundle, this) : bundle); } /** @@ -1124,4 +1262,25 @@ public boolean disable(final ItemInfo item) { return test; } } + + /** + * Delayed configuration action with preserved source context. + */ + private class DelayedConfig { + private final ItemId scope; + private final Consumer action; + + DelayedConfig(final ItemId scope, final Consumer action) { + this.scope = scope; + this.action = action; + } + + public void process(final GuiceyEnvironment environment) { + final ItemId original = currentScope; + // change scope to indicate actual registration scope (diff. app scope and hook) + currentScope = scope; + action.accept(environment); + currentScope = original; + } + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java similarity index 86% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java index 6f375b6cb..8a50768bc 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/ConfigurationInfo.java @@ -4,6 +4,7 @@ import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.impl.ItemInfoImpl; @@ -43,7 +44,15 @@ public final class ConfigurationInfo { // preserve all instance types together private final Multimap, ItemInfo> instanceTypes = LinkedHashMultimap.create(); + private final List> hooks; + + /** + * Create configuration info. + * + * @param context configuration context + */ public ConfigurationInfo(final ConfigurationContext context) { + hooks = context.getExecutedHookTypes(); // convert all objects into types (more suitable for analysis) for (ConfigItem type : ConfigItem.values()) { for (Object item : context.getItems(type)) { @@ -103,7 +112,7 @@ public List> getItems(final ConfigItem type, f * @param filter predicate to filter definitions * @return registered item ids in registration order, filtered with provided filter or empty list */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "PMD.UseDiamondOperator"}) public List> getItems(final Predicate filter) { final List> items = new ArrayList(itemsHolder.values()); return filter(items, filter); @@ -176,14 +185,40 @@ public List getInfos(final Class type) { final ItemId id = ItemId.from(type); // it may be request for class item or for all instance items (of provided class) if (instanceTypes.containsKey(id.getType())) { - return new ArrayList((Collection) instanceTypes.get(id.getType())); + return new ArrayList<>((Collection) instanceTypes.get(id.getType())); } // if item is only disabled without actual registration final T res = (T) classTypes.get(id); return res == null ? Collections.emptyList() : Collections.singletonList(res); } + /** + * The simple way to receive a large set of info objects, instead of just ids. Useful for sorting. + * + * @param type required item type + * @param filter filter + * @param item type + * @return all registrations matching filter or empty list if nothing registered + */ + public List getInfos(final ConfigItem type, final Predicate filter) { + return filterInfos(getItems(type), filter); + } + + /** + * @return types of executed hooks + */ + public List> getHooks() { + return hooks; + } + private List> filter(final List> items, final Predicate filter) { return items.stream().filter(it -> filter.test(getInfo(it))).collect(Collectors.toList()); } + + @SuppressWarnings("unchecked") + private List filterInfos(final List> items, final Predicate filter) { + return items.stream() + .map(itemId -> (K) getInfo(itemId)) + .filter(filter).collect(Collectors.toList()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java similarity index 59% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java index 32bf3b3f5..cb498c1ce 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Disables.java @@ -1,9 +1,16 @@ package ru.vyarus.dropwizard.guice.module.context; +import ru.vyarus.dropwizard.guice.module.context.info.DropwizardBundleItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.GuiceyBundleItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.InstallerItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ModuleItemInfo; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import java.util.Arrays; +import java.util.List; import java.util.function.Predicate; /** @@ -31,7 +38,8 @@ public static Predicate registeredBy(final ConfigScope... types) { /** * Check registration source. Context class could be - * {@link io.dropwizard.Application}, {@link ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner}, + * {@link io.dropwizard.core.Application}, + * {@link ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner}, * {@link ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup} and classes implementing * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle}. * @@ -44,6 +52,10 @@ public static Predicate registeredBy(final Class... types) { return input -> Arrays.asList(types).contains(input.getRegistrationScope().getType()); } + /** + * @param scopes registration scope + * @return predicate for items registered in scope + */ public static Predicate registeredBy(final ItemId... scopes) { // in time of disable predicate run registration scope == registered by return input -> Arrays.asList(scopes).contains(input.getRegistrationScope()); @@ -53,32 +65,43 @@ public static Predicate registeredBy(final ItemId... scopes) { * Generic item type predicate. It could be installer, bundle, extension or module. * * @param types configuration types to match + * @param expected info container type (if used within single configuration type) * @return items of specific type predicate */ - public static Predicate itemType(final ConfigItem... types) { + public static Predicate itemType(final ConfigItem... types) { return Filters.type(types); } /** * @return extension item predicate */ - public static Predicate extension() { + public static Predicate extension() { return itemType(ConfigItem.Extension); } + /** + * @param installers installers + * @return predicate for extensions installed by provided installer + */ + @SafeVarargs + public static Predicate installedBy(final Class... installers) { + final List> deny = Arrays.asList(installers); + return extension().and(info -> deny.contains(info.getInstalledBy())); + } + /** * Note that only directly registered modules are covered. * * @return guice module item predicate */ - public static Predicate module() { + public static Predicate module() { return itemType(ConfigItem.Module); } /** * @return guicey bundle item predicate */ - public static Predicate bundle() { + public static Predicate bundle() { return itemType(ConfigItem.Bundle); } @@ -87,14 +110,14 @@ public static Predicate bundle() { * * @return guicey dropwizard bundle item predicate */ - public static Predicate dropwizardBundle() { + public static Predicate dropwizardBundle() { return itemType(ConfigItem.DropwizardBundle); } /** * @return installer item predicate */ - public static Predicate installer() { + public static Predicate installer() { return itemType(ConfigItem.Installer); } @@ -118,4 +141,25 @@ public static Predicate inPackage(final String... pkgs) { return Arrays.stream(pkgs).anyMatch(typePkg::startsWith); }; } + + /** + * Web extensions are all extensions activated with jetty (including jersey extensions like rest resources). + * (web extensions identified by installers, implementing + * {@link ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller}) + * + * @return web extensions predicate + */ + public static Predicate webExtension() { + return extension().and(ExtensionItemInfo::isWebExtension); + } + + /** + * Jersey extensions are extensions installed with + * {@link ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller}. + * + * @return jersey extensions predicate + */ + public static Predicate jerseyExtension() { + return extension().and(ExtensionItemInfo::isJerseyExtension); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java similarity index 81% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java index b7622e568..a4d526f3f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/Filters.java @@ -19,6 +19,7 @@ * @author Vyacheslav Rusakov * @since 06.07.2016 */ +@SuppressWarnings("PMD.TooManyMethods") public final class Filters { private Filters() { @@ -38,6 +39,19 @@ public static Predicate enabled() { return input -> !(input instanceof DisableSupport) || ((DisableSupport) input).isEnabled(); } + /** + * Filter for disabled items. Not all items support disable ({@link DisableSupport}). + * Items not supporting disable considered enabled (so it's safe to apply filter for + * all items). + * + * @param expected info container type (if used within single configuration type) + * @return enabled items filter + */ + @SuppressWarnings("unchecked") + public static Predicate disabled() { + return ((Predicate) enabled()).negate(); + } + /** * Filter for items disabled in specified scope. Not all items support disable ({@link DisableSupport}). * Items not supporting disable considered enabled (so it's safe to apply filter for @@ -76,7 +90,8 @@ public static Predicate fromScan() { /** * Filter for items registered by specified context. Context class could be - * {@link io.dropwizard.Application}, {@link ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner}, + * {@link io.dropwizard.core.Application}, + * {@link ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner}, * {@link ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup} and * classes implementing {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle}. * Safe to apply filter for all items. @@ -212,8 +227,29 @@ public static Predicate bundles() { return Filters.type(ConfigItem.Bundle).or(type(ConfigItem.DropwizardBundle)); } + /** + * @return guicey bundles predicate + */ + public static Predicate guiceyBundles() { + return type(ConfigItem.Bundle); + } + + /** + * @return dropwizard bundles predicate + */ + public static Predicate dropwizardBundles() { + return type(ConfigItem.DropwizardBundle); + } + // --------------------------------------------------------------------------- EXTENSIONS + /** + * @return extensions predicate + */ + public static Predicate extensions() { + return type(ConfigItem.Extension); + } + /** * Filter for extensions installed by specified installer. Use only for {@link ConfigItem#Extension} items. * @@ -233,8 +269,34 @@ public static Predicate fromBinding() { return ExtensionItemInfo::isGuiceBinding; } + /** + * Filter for web extensions (everything web-related, including jersey extensions). Use only for + * {@link ConfigItem#Extension} items. + * + * @return web extensions matcher + */ + public static Predicate webExtension() { + return ExtensionItemInfo::isWebExtension; + } + + /** + * Filter for jersey extensions. Use only for {@link ConfigItem#Extension} items. + * + * @return jersey extensions matcher + */ + public static Predicate jerseyExtension() { + return ExtensionItemInfo::isJerseyExtension; + } + // --------------------------------------------------------------------------- MODULES + /** + * @return guice modules predicate + */ + public static Predicate modules() { + return type(ConfigItem.Module); + } + /** * Filter for overriding modules. Use only for {@link ConfigItem#Module} items. * @@ -243,4 +305,13 @@ public static Predicate fromBinding() { public static Predicate overridingModule() { return ModuleItemInfo::isOverriding; } + + // --------------------------------------------------------------------------- INSTALLERS + + /** + * @return installers predicate + */ + public static Predicate installers() { + return type(ConfigItem.Installer); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/OptionalExtensionDisablerScope.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/OptionalExtensionDisablerScope.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/OptionalExtensionDisablerScope.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/OptionalExtensionDisablerScope.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java similarity index 62% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java index 655db3991..724bbe1f1 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/SharedConfigurationState.java @@ -3,34 +3,43 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.google.inject.Injector; -import io.dropwizard.Application; -import io.dropwizard.Configuration; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import io.dropwizard.lifecycle.Managed; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import jakarta.inject.Provider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.installer.util.StackUtils; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; -import javax.inject.Provider; -import java.util.HashMap; +import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; /** - * Application-wide shared state assumed to be used in configuration phase. For example, + * Application-wide shared state assumed to be used in the configuration phase. For example, * in complex cases when bundles must share some global state (otherwise it would require to maintain - * some {@link ThreadLocal} field in bundle). But other cases could arise too. Intended to be used for very rare cases: - * use it only if you can't avoid shared state in configuration time (for example, like guicey gsp bundle, + * some {@link ThreadLocal} field in a bundle). But other cases could arise too. Intended to be used for very rare + * cases: use it only if you can't avoid shared state in configuration time (for example, like the guicey gsp bundle, * which requires global configuration, accessible by multiple bundles). *

    * Internally, guicey use it to store created {@link com.google.inject.Injector} and make it available statically * (see {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup}). *

    - * Universal sharing place should simplify testing in the future: if new global state will appear, there would be no + * Universal sharing place should simplify testing in the future: if a new global state appeared, there would be no * need to reset anything in tests (to clear state, for example, on testing errors). One "dirty" place to replace * all separate hacks. *

    @@ -44,19 +53,26 @@ * from application main thread. This might be required for places where neither application nor environment * object available. After guice bundle's run method finishes, startup instance is unlinked. *

    - * Classes are used as state keys to simplify usage (in most cases, bundle class will be used as key). + * Classes are used as state keys to simplify usage (shared object class is the key). * Shared value could be set only once (to prevent complex situations with state substitutions). It is advised * to initialize shared value only in initialization phase (to avoid potential static access errors). *

    - * Objects available in shared state by default: {@link io.dropwizard.setup.Bootstrap}, - * {@link io.dropwizard.Configuration}, {@link Environment}, - * {@link ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree}, {@link com.google.inject.Injector} - * (see shortcut instance methods). + * If stored state object implements {@link AutoCloseable} it will be automatically closed on application shutdown + * (which is closed in most cases under tests). This is mostly useful to guarantee resource cleanup after the test + * (when it can't be managed directly). + *

    + * Objects available in shared state by default: {@link io.dropwizard.core.setup.Bootstrap}, + * {@link io.dropwizard.core.Configuration}, {@link Environment}, + * {@link ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree}, {@link com.google.inject.Injector}, + * {@link ru.vyarus.dropwizard.guice.module.context.option.Options} (see shortcut instance methods). + *

    + * To debug shared state access use {@link #getAccessReport()} + * (or {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#printSharedStateUsage()}). * * @author Vyacheslav Rusakov * @since 26.09.2019 */ -@SuppressWarnings("rawtypes") +@SuppressWarnings({"rawtypes", "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods"}) public class SharedConfigurationState { /** * Attribute name used to store application instance in application context attributes. @@ -66,31 +82,60 @@ public class SharedConfigurationState { private static final Map STATE = Maps.newConcurrentMap(); /** - * During application startup all initialization performed in the single thread and so it is possible - * to reference shared state instance with a simple static call. This is required because in some cases - * neither Application nor Environment objects are accessible (e.g. BindingInstaller, BundlesLookup). + * During application startup all initialization performed in the single thread, and so it is possible + * to reference shared state instance with a simple static call. This is required because in some cases, + * neither Application nor Environment objects are accessible (e.g., BindingInstaller, BundlesLookup). */ private static final ThreadLocal STARTUP_INSTANCE = new ThreadLocal<>(); - private final Map state = new HashMap<>(); + private final Logger logger = LoggerFactory.getLogger(SharedConfigurationState.class); + + // string used as key to workaround potential problems with different class loaders + private final Map state = new LinkedHashMap<>(); + + // put calls + private final Map statePopulationTrack = new LinkedHashMap<>(); + // get calls (including misses) + private final Multimap stateAccessTrack = LinkedHashMultimap.create(); + // listener registration (to show state delayed access correctly) + private final Map listenersTrack = new LinkedHashMap<>(); + // reactive values + // NOTE: no validation for not called listeners to allow optional reactive states + private final Multimap listeners = LinkedHashMultimap.create(); private Application application; + /** + * Create state. + */ public SharedConfigurationState() { // make shared state accessible during startup in all places STARTUP_INSTANCE.set(this); } /** - * Note: in spite of fact that class is used for key, actual value is stored with full class name string. + * Note: although class is used for key, actual value is stored with full class name string. * So classes, loaded from different class loaders will lead to the same value. * * @param key shared object key * @param shared object type * @return value or null */ + public V get(final Class key) { + return get(key.getName()); + } + + /** + * Special string-based state access for revising state with {@link #getKeys()}. + * + * @param key state key + * @param value type + * @return state value or null + */ @SuppressWarnings("unchecked") - public V get(final Class key) { - return (V) state.get(key.getName()); + public V get(final String key) { + final V v = (V) state.get(key); + stateAccessTrack.put(key, (v == null ? "MISS " : "GET ") + StackUtils.getSharedStateSource()); + return v; } /** @@ -101,27 +146,25 @@ public V get(final Class key) { * @param shared object type * @return stored or default (just stored) value */ - public V get(final Class key, final Supplier defaultValue) { - V res = get(key); - if (res == null && defaultValue != null) { - res = defaultValue.get(); - put(key, res); + public V get(final Class key, final Supplier defaultValue) { + if (!state.containsKey(key.getName())) { + put(key, defaultValue.get()); } - return res; + return get(key); } /** * Shortcut for {@link #get(Class)} to immediately fail if value not set. Supposed to be used by shared state - * consumers (to validate situations when value must exists for sure). + * consumers (to validate situations when value must exist for sure). * * @param key shared object key * @param message exception message (could use {@link String#format(String, Object...)} placeholders) * @param args placeholder arguments for error message * @param shared object type * @return stored object (never null) - * @throws IllegalStateException if value not set + * @throws IllegalStateException if value isn't set */ - public V getOrFail(final Class key, final String message, final Object... args) { + public V getOrFail(final Class key, final String message, final Object... args) { final V res = get(key); if (res == null) { throw new IllegalStateException(Strings.lenientFormat(message, args)); @@ -129,8 +172,37 @@ public V getOrFail(final Class key, final String message, final Object... return res; } + /** + * Reactive shared value access: if value already available action called immediately, otherwise action would + * be called when value set (note that value could be set only once). + *

    + * Note: listener would not be called if the state is never set. This assumed to be used for optional state cases. + * + * @param key shared object key + * @param action action to execute when value would be set + * @param value type + */ + public void whenReady(final Class key, final Consumer action) { + final String name = key.getName(); + if (state.containsKey(name)) { + action.accept(get(name)); + } else { + listeners.put(name, action); + listenersTrack.put(action, StackUtils.getSharedStateSource()); + } + } + // ---- providers for common objects + /** + * Options access object is always available. + * + * @return options access object + */ + public Options getOptions() { + return Preconditions.checkNotNull(get(Options.class), "Options object not yet available"); + } + /** * Bootstrap object is available since guice bundle initialization start. It will not be available for * hooks (because hooks processed before guice bundle initialization call - no way to get boostrap reference). @@ -138,6 +210,7 @@ public V getOrFail(final Class key, final String message, final Object... * @param configuration type * @return bootstrap instance provider (would fail if called too early) */ + @SuppressWarnings("unchecked") public Provider> getBootstrap() { return () -> Preconditions.checkNotNull(get(Bootstrap.class), "Bootstrap object not yet available"); } @@ -150,7 +223,7 @@ public Provider> getBootstrap() { */ @SuppressWarnings("unchecked") public Provider> getApplication() { - return () -> Optional.ofNullable(get(Bootstrap.class)) + return () -> Optional.ofNullable(get(Bootstrap.class)) .map(b -> (Application) b.getApplication()) .orElseThrow(() -> new NullPointerException("Application instance is not yet available")); } @@ -171,8 +244,9 @@ public Provider getEnvironment() { * @param configuration type * @return configuration instance provider (would fail if called too early) */ + @SuppressWarnings("unchecked") public Provider getConfiguration() { - return () -> Preconditions.checkNotNull(get(Configuration.class), + return () -> (C) Preconditions.checkNotNull(get(Configuration.class), "Configuration is not yet available"); } @@ -193,7 +267,7 @@ public Provider getConfigurationTree() { * @see ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup for simpler lookup method */ public Provider getInjector() { - return () -> Preconditions.checkNotNull(get(ConfigurationTree.class), + return () -> Preconditions.checkNotNull(get(Injector.class), "Injector is not yet available"); } @@ -211,14 +285,22 @@ public Provider getInjector() { * * @param key shared object key * @param value shared value (usually configuration object) + * @param value type */ - public void put(final Class key, final Object value) { + @SuppressWarnings("unchecked") + public void put(final Class key, final V value) { Preconditions.checkArgument(key != null, "Shared state key can't be null"); // just to avoid dummy mistakes Preconditions.checkArgument(value != null, "Shared state does not accept null values"); final String name = key.getName(); Preconditions.checkState(!state.containsKey(name), "Shared state for key %s already defined", name); state.put(name, value); + statePopulationTrack.put(name, StackUtils.getSharedStateSource()); + // processed one time + listeners.removeAll(name).forEach(consumer -> { + consumer.accept(value); + stateAccessTrack.put(name, "GET " + listenersTrack.get(consumer)); + }); } /** @@ -230,6 +312,29 @@ public Set getKeys() { return new HashSet<>(state.keySet()); } + /** + * Clear state for exact application. Normally, state should shut down automatically (as it is a managed object). + * But in tests with disabled lifecycle this will not happen and so cleanup must be manual. + * + * @see SharedConfigurationState#clear() to remove all states (for tests) + */ + @VisibleForTesting + public void shutdown() { + getKeys().forEach(key -> { + final Object res = get(key); + if (res instanceof AutoCloseable) { + try { + ((AutoCloseable) res).close(); + } catch (Exception ex) { + logger.warn("Problem closing object '" + key + "' in shared state", ex); + } + } + }); + if (application != null) { + STATE.remove(application); + } + } + @Override public String toString() { return "Shared state with " + state.size() + " objects: " + String.join(", ", getKeys()); @@ -251,12 +356,12 @@ protected void assignTo(final Application application) { /** * Called on run phase to assign to application lifecycle and listen for shutdown. * - * @param environment environment object + * @param environment environment object */ protected void listen(final Environment environment) { // storing application reference in context attributes (to be able to reference shared state by environment) environment.getApplicationContext().setAttribute(CONTEXT_APPLICATION_PROPERTY, application); - environment.lifecycle().manage(new RegistryShutdown(application)); + environment.lifecycle().manage(new RegistryShutdown(this)); } /** @@ -295,7 +400,7 @@ public static SharedConfigurationState getStartupInstance() { * @param shared object key * @return value optional */ - public static Optional lookup(final Application application, final Class key) { + public static Optional lookup(final Application application, final Class key) { return get(application).map(value -> value.get(key)); } @@ -307,7 +412,7 @@ public static Optional lookup(final Application application, final Class< * @param shared object key * @return value optional */ - public static Optional lookup(final Environment environment, final Class key) { + public static Optional lookup(final Environment environment, final Class key) { return get(environment).map(value -> value.get(key)); } @@ -322,12 +427,11 @@ public static Optional lookup(final Environment environment, final Class< * @return value (never null) * @throws IllegalStateException if value not available */ - @SuppressWarnings("unchecked") public static V lookupOrFail(final Application application, - final Class key, + final Class key, final String message, final Object... args) { - return ((Optional) lookup(application, key)) + return lookup(application, key) .orElseThrow(() -> new IllegalStateException(Strings.lenientFormat(message, args))); } @@ -342,15 +446,46 @@ public static V lookupOrFail(final Application application, * @return value (never null) * @throws IllegalStateException if value not available */ - @SuppressWarnings("unchecked") public static V lookupOrFail(final Environment environment, - final Class key, + final Class key, final String message, final Object... args) { - return ((Optional) lookup(environment, key)) + return lookup(environment, key) .orElseThrow(() -> new IllegalStateException(Strings.lenientFormat(message, args))); } + /** + * Shortcut for {@link #lookup(Application, Class)} to get ot initialize shared state value. + * + * @param application application instance + * @param key shared object key + * @param defSupplier default value supplier (used when state is not exists to initialize it) + * @param shared object type + * @return value (never null) + * @throws IllegalStateException if value not available + */ + public static V lookupOrCreate(final Application application, + final Class key, + final Supplier defSupplier) { + return getOrFail(application, "State is not available yet").get(key, defSupplier); + } + + /** + * Shortcut for {@link #lookup(Environment, Class)} to get ot initialize shared state value. + * + * @param environment environment instance + * @param key shared object key + * @param defSupplier default value supplier (used when state is not exists to initialize it) + * @param shared object type + * @return value (never null) + * @throws IllegalStateException if value not available + */ + public static V lookupOrCreate(final Environment environment, + final Class key, + final Supplier defSupplier) { + return getOrFail(environment, "State is not available yet").get(key, defSupplier); + } + /** * Static lookup for entire application registry. * @@ -418,6 +553,7 @@ public static SharedConfigurationState getOrFail(final Environment environment, */ @VisibleForTesting public static void clear() { + STATE.values().forEach(SharedConfigurationState::shutdown); STATE.clear(); } @@ -431,26 +567,70 @@ public static int statesCount() { return STATE.size(); } + /** + * Shows all objects in state with a link to assignment source and all state access attempts (success or misses). + * + * @return shared state usage report + */ + public String getAccessReport() { + final StringBuilder report = new StringBuilder(200); + boolean blankLineAdded = false; + for (Map.Entry entry : statePopulationTrack.entrySet()) { + final String k = entry.getKey(); + final String value = entry.getValue(); + final Collection gets = stateAccessTrack.get(k); + report.append(!blankLineAdded && (report.isEmpty() || !gets.isEmpty()) ? "\n" : "").append("\tSET ") + .append(String.format("%-80s\t %s%n", renderKey(k), value)); + blankLineAdded = false; + gets.forEach(s -> report.append("\t\t").append(s).append('\n')); + if (!gets.isEmpty()) { + report.append('\n'); + blankLineAdded = true; + } + } + + final Set notset = new LinkedHashSet<>(stateAccessTrack.keySet()); + notset.removeAll(statePopulationTrack.keySet()); + notset.forEach(key -> { + report.append("\n\tNEVER SET ").append(renderKey(key)).append('\n'); + stateAccessTrack.get(key).forEach(s -> report.append("\t\t").append(s).append('\n')); + // never called listeners + listeners.get(key).forEach(consumer -> + report.append("\t\tMISS ").append(listenersTrack.get(consumer)).append('\n')); + }); + + // listeners-only missed access + listeners.keySet().forEach(key -> { + if (!notset.contains(key)) { + report.append("\n\tNEVER SET ").append(renderKey(key)).append('\n'); + listeners.get(key).forEach(consumer -> + report.append("\t\tMISS ").append(listenersTrack.get(consumer)).append('\n')); + } + }); + + return report.toString(); + } + + private String renderKey(final String key) { + final int lastDot = key.lastIndexOf('.'); + return key.substring(lastDot + 1) + " (" + key.substring(0, lastDot) + ")"; + } + /** * Remove global registration on shutdown. This is actually not important for real application. * Only could be sensible in tests when many application instances could be created (and not released without * this hook). */ private static class RegistryShutdown implements Managed { - private final Application application; + private final SharedConfigurationState state; - protected RegistryShutdown(final Application application) { - this.application = application; - } - - @Override - public void start() throws Exception { - // not used + protected RegistryShutdown(final SharedConfigurationState state) { + this.state = state; } @Override public void stop() throws Exception { - STATE.remove(application); + state.shutdown(); } } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java index 822aba4a4..b84062ad0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/BootstrapProxyFactory.java @@ -1,11 +1,12 @@ package ru.vyarus.dropwizard.guice.module.context.bootstrap; -import io.dropwizard.Application; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.Application; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; import javassist.util.proxy.Proxy; import javassist.util.proxy.ProxyFactory; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; /** * {@link Bootstrap} proxy delegates all calls directly to bootstrap object, except bundle addition. Instead, @@ -32,7 +33,7 @@ public static Bootstrap create(final Bootstrap bootstrap, final ConfigurationCon factory.setSuperclass(Bootstrap.class); final Class proxy = factory.createClass(); - final Bootstrap res = (Bootstrap) proxy.getConstructor(Application.class).newInstance(new Object[]{null}); + final Bootstrap res = (Bootstrap) InstanceUtils.createWithNulls(proxy, Application.class); ((Proxy) res).setHandler((self, thisMethod, proceed, args) -> { // intercept only bundle addition if ("addBundle".equals(thisMethod.getName())) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java similarity index 82% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java index 1c165494f..3cec29054 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/bootstrap/DropwizardBundleTracker.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.module.context.bootstrap; -import io.dropwizard.Configuration; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; @@ -13,15 +13,21 @@ * Bundles tracking is controlled with {@link ru.vyarus.dropwizard.guice.GuiceyOptions#TrackDropwizardBundles} * option. * + * @param configuration type * @author Vyacheslav Rusakov * @since 07.05.2019 - * @param configuration type */ public class DropwizardBundleTracker implements ConfiguredBundle { private final ConfiguredBundle bundle; private final ConfigurationContext context; + /** + * Create tracker. + * + * @param bundle dropwizard bundle + * @param context configuration context + */ public DropwizardBundleTracker(final ConfiguredBundle bundle, final ConfigurationContext context) { this.bundle = bundle; this.context = context; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/BundleItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/BundleItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/BundleItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/BundleItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ClassItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ClassItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ClassItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ClassItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/CommandItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/CommandItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/CommandItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/CommandItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java index 6c67727a3..9a6635285 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/DropwizardBundleItemInfo.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.context.info; -import io.dropwizard.ConfiguredBundle; +import io.dropwizard.core.ConfiguredBundle; /** * Dropwizard bundle configuration information. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java similarity index 67% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java index 2ed2fd651..722b277dd 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ExtensionItemInfo.java @@ -30,12 +30,12 @@ public interface ExtensionItemInfo extends ClassItemInfo, ScanSupport, DisableSu /** * Indicates extension management by jersey instead of guice. + *

    + * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @return true if extension annotated with * {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged}, false otherwise - * @deprecated in the next version HK2 support will be removed and annotation will become useless */ - @Deprecated boolean isJerseyManaged(); /** @@ -54,4 +54,23 @@ public interface ExtensionItemInfo extends ClassItemInfo, ScanSupport, DisableSu * @return true if extension is optional */ boolean isOptional(); + + /** + * Web extensions detected by {@link ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller} marker + * interface on detected installer. Web extensions are: resources, servlets, filters, rest and all related + * jersey extensions. Everything that is not starts with lightweight guicey test should be marked as web. + * + * @return true if extension is a web extension + */ + boolean isWebExtension(); + + /** + * Jersey extensions detected by {@link ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller} + * interface. Note that jersey extensions are {@link #isWebExtension()}. + *

    + * Don't confuse with {@link #isJerseyManaged()} which indicates what ioc manage extension instance (guice/hk2). + * + * @return true if extension is a jersey extension + */ + boolean isJerseyExtension(); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java similarity index 76% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java index 44407b2df..ab0b1aa09 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/GuiceyBundleItemInfo.java @@ -10,7 +10,7 @@ * Note that the same bundle may be registered by different mechanism simultaneously. * For example: by lookup and manually in application class. Bundle will actually be registered either only once * (if correct equals method implemented) and it's info will contain 2 context classes - * ({@link io.dropwizard.Application} and {@link ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup}) + * ({@link io.dropwizard.core.Application} and {@link ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup}) * (and {@link #isFromLookup()} will be true) or as separate instances. * * @author Vyacheslav Rusakov @@ -23,4 +23,11 @@ public interface GuiceyBundleItemInfo extends BundleItemInfo { * @see ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup */ boolean isFromLookup(); + + /** + * Useful for sorting bundles in initialization order (to correctly order transitive bundles). + * + * @return initialization order (starting from 1) + */ + int getInitOrder(); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstallerItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstallerItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstallerItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstallerItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstanceItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstanceItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstanceItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/InstanceItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java index e80712eda..f26b46040 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemId.java @@ -80,7 +80,7 @@ public static String identity(final Object instance) { public static ItemId from(final Object instance) { return instance instanceof Class ? from((Class) instance) - : new ItemId(instance.getClass(), identity(instance)); + : new ItemId<>(instance.getClass(), identity(instance)); } /** @@ -91,7 +91,7 @@ public static ItemId from(final Object instance) { * @return type key */ public static ItemId from(final Class type) { - return new ItemId(type, null); + return new ItemId<>(type, null); } /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java index bbec65f4c..05f9838ee 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ItemInfo.java @@ -40,7 +40,7 @@ public interface ItemInfo { /** * Configuration items may be registered by root application class, classpath scan or guicey bundle. - * For registrations in application class {@link io.dropwizard.Application} is stored as context. + * For registrations in application class {@link io.dropwizard.core.Application} is stored as context. * For registration by classpath scan {@link ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner} * is stored as context. For registrations by * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle}, actual bundle class is stored @@ -119,4 +119,12 @@ public interface ItemInfo { * @return number of ignored items in all scopes of specified type */ int getIgnoresByScope(Class scope); + + /** + * For example, item for extension might be registered before recognition with installer, and it is + * important to wait for installer before applying disable predicates (which potentially may rely on installer). + * + * @return true if item completely initialized (all related data provided) + */ + boolean isAllDataCollected(); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ModuleItemInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ModuleItemInfo.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ModuleItemInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/ModuleItemInfo.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java similarity index 79% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java index 5210aeff3..c7e31021b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/BundleItemInfoImpl.java @@ -18,11 +18,22 @@ public abstract class BundleItemInfoImpl extends InstanceItemInfoImpl impl private final Set disabledBy = Sets.newLinkedHashSet(); - // disable only + /** + * Create disable-only item (only indicates disabling). + * + * @param type bundle type + * @param item bundle class + */ public BundleItemInfoImpl(final ConfigItem type, final Class item) { super(type, item); } + /** + * Create bundle item. + * + * @param type bundle type + * @param instance bundle instance + */ public BundleItemInfoImpl(final ConfigItem type, final T instance) { super(type, instance); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java index 7d4a18af9..f93398c1b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ClassItemInfoImpl.java @@ -12,6 +12,12 @@ */ public abstract class ClassItemInfoImpl extends ItemInfoImpl implements ClassItemInfo { + /** + * Create item info. + * + * @param itemType item type + * @param type item class + */ public ClassItemInfoImpl(final ConfigItem itemType, final Class type) { super(itemType, ItemId.from(type)); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java similarity index 86% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java index 469c260d0..fb89534d4 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/CommandItemInfoImpl.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.context.info.impl; -import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.core.cli.EnvironmentCommand; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigScope; import ru.vyarus.dropwizard.guice.module.context.info.CommandItemInfo; @@ -13,6 +13,11 @@ */ public class CommandItemInfoImpl extends ClassItemInfoImpl implements CommandItemInfo { + /** + * Create item. + * + * @param type command type + */ public CommandItemInfoImpl(final Class type) { super(ConfigItem.Command, type); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java similarity index 80% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java index 4a1669d50..adaedf335 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/DropwizardBundleItemInfoImpl.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.context.info.impl; -import io.dropwizard.ConfiguredBundle; +import io.dropwizard.core.ConfiguredBundle; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigScope; import ru.vyarus.dropwizard.guice.module.context.info.DropwizardBundleItemInfo; @@ -14,11 +14,20 @@ public class DropwizardBundleItemInfoImpl extends BundleItemInfoImpl implements DropwizardBundleItemInfo { - // disable only + /** + * Create disable-only bundle info (only disable marker). + * + * @param type bundle type + */ public DropwizardBundleItemInfoImpl(final Class type) { super(ConfigItem.DropwizardBundle, type); } + /** + * Create bundle item info. + * + * @param bundle bundle instance + */ public DropwizardBundleItemInfoImpl(final ConfiguredBundle bundle) { super(ConfigItem.DropwizardBundle, bundle); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java similarity index 69% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java index f7620d327..ea9d0f5c4 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ExtensionItemInfoImpl.java @@ -1,5 +1,6 @@ package ru.vyarus.dropwizard.guice.module.context.info.impl; +import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.inject.Binding; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; @@ -7,6 +8,8 @@ import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller; import java.util.Set; @@ -27,8 +30,15 @@ public class ExtensionItemInfoImpl extends ClassItemInfoImpl implements Extensio private FeatureInstaller installer; private boolean optional; + /** + * Create item. + * + * @param type extension type + */ public ExtensionItemInfoImpl(final Class type) { super(ConfigItem.Extension, type); + // not initialized while installer not set + this.complete = false; } @Override @@ -71,32 +81,64 @@ public boolean isOptional() { return optional; } + /** + * @param lazy lazy extension + */ public void setLazy(final boolean lazy) { this.lazy = lazy; } + /** + * @param jerseyManaged jersey managed extension + */ public void setJerseyManaged(final boolean jerseyManaged) { this.jerseyManaged = jerseyManaged; } + /** + * @param manualBinding manually declared guice binding + */ public void setManualBinding(final Binding manualBinding) { this.manualBinding = manualBinding; } + /** + * @return manually declared guice binding + */ public Binding getManualBinding() { return manualBinding; } + /** + * @return installer recognized extension + */ public FeatureInstaller getInstaller() { return installer; } + /** + * @param installer installer recognized extension + */ public void setInstaller(final FeatureInstaller installer) { this.installer = installer; this.installedBy = installer.getClass(); + this.complete = true; } + /** + * @param optional optional extension + */ public void setOptional(final boolean optional) { this.optional = optional; } + + @Override + public boolean isWebExtension() { + return WebInstaller.class.isAssignableFrom(Preconditions.checkNotNull(installedBy)); + } + + @Override + public boolean isJerseyExtension() { + return JerseyInstaller.class.isAssignableFrom(Preconditions.checkNotNull(installedBy)); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java similarity index 72% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java index e5272bc15..bb3623fd6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/GuiceyBundleItemInfoImpl.java @@ -13,11 +13,22 @@ */ public class GuiceyBundleItemInfoImpl extends BundleItemInfoImpl implements GuiceyBundleItemInfo { - // disable only + private int initOrder; + + /** + * Create disable-only bundle item (only disabled). + * + * @param type bundle type + */ public GuiceyBundleItemInfoImpl(final Class type) { super(ConfigItem.Bundle, type); } + /** + * Create bundle info. + * + * @param bundle guicey bundle + */ public GuiceyBundleItemInfoImpl(final GuiceyBundle bundle) { super(ConfigItem.Bundle, bundle); } @@ -38,4 +49,16 @@ public boolean isTransitive() { public boolean isDropwizard() { return false; } + + @Override + public int getInitOrder() { + return initOrder; + } + + /** + * @param initOrder initialization order + */ + public void setInitOrder(final int initOrder) { + this.initOrder = initOrder; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java index 875b8ccf7..02c48d547 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstallerItemInfoImpl.java @@ -17,6 +17,11 @@ public class InstallerItemInfoImpl extends ClassItemInfoImpl implements InstallerItemInfo { private final Set disabledBy = Sets.newLinkedHashSet(); + /** + * Create item. + * + * @param type installer type + */ public InstallerItemInfoImpl(final Class type) { super(ConfigItem.Installer, type); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java similarity index 80% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java index 73fa917ee..0108145ea 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/InstanceItemInfoImpl.java @@ -10,8 +10,8 @@ /** * Instance item info implementation. * - * @author Vyacheslav Rusakov * @param instance type + * @author Vyacheslav Rusakov * @since 04.07.2019 */ public abstract class InstanceItemInfoImpl extends ItemInfoImpl implements InstanceItemInfo { @@ -20,12 +20,23 @@ public abstract class InstanceItemInfoImpl extends ItemInfoImpl implements In private int instanceCount; private final List duplicates = new ArrayList<>(); - // special constructor for disable-only items (without actual registration) + /** + * Create disable-only info (special constructor for disable-only items (without actual registration). + * + * @param itemType item type + * @param type item class + */ public InstanceItemInfoImpl(final ConfigItem itemType, final Class type) { super(itemType, ItemId.from(type)); this.instance = null; } + /** + * Create item. + * + * @param itemType item type + * @param instance item instance + */ public InstanceItemInfoImpl(final ConfigItem itemType, final T instance) { super(itemType, ItemId.from(instance)); this.instance = instance; @@ -41,6 +52,9 @@ public int getInstanceCount() { return instanceCount; } + /** + * @param instanceCount instances count + */ public void setInstanceCount(final int instanceCount) { this.instanceCount = instanceCount; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java index b7f0d5b65..f57407a47 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ItemInfoImpl.java @@ -17,6 +17,11 @@ * @since 06.07.2016 */ public class ItemInfoImpl implements ItemInfo { + /** + * Indicates complete initialization (useful for extensions - installer might be not set). + */ + protected boolean complete = true; + private final ItemId id; private final ConfigItem itemType; private final Set registeredBy = Sets.newLinkedHashSet(); @@ -25,6 +30,12 @@ public class ItemInfoImpl implements ItemInfo { // registrations per scope (actual + ignored) private final InstanceCounter counter = new InstanceCounter(); + /** + * Create item. + * + * @param itemType item type + * @param id item id + */ public ItemInfoImpl(final ConfigItem itemType, final ItemId id) { this.itemType = itemType; this.id = id; @@ -104,6 +115,16 @@ public int getIgnoresByScope(final Class scope) { return getIgnoresByScope(ItemId.from(scope)); } + @Override + public boolean isAllDataCollected() { + return complete; + } + + /** + * Record item registration. + * + * @param scope registration scope + */ public void countRegistrationAttempt(final ItemId scope) { registrationAttempts++; if (registrationScope == null) { @@ -121,7 +142,7 @@ public String toString() { /** * Counts instance appearances by scope. */ - private static class InstanceCounter { + private static final class InstanceCounter { private final Map counts = new HashMap<>(); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java index 0f4568489..4021e7ff2 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/impl/ModuleItemInfoImpl.java @@ -21,12 +21,21 @@ public class ModuleItemInfoImpl extends InstanceItemInfoImpl implements private final Set disabledBy = Sets.newLinkedHashSet(); private final boolean overriding; - // disable only item + /** + * Create disabled-only item (without an actual item). + * + * @param type item type + */ public ModuleItemInfoImpl(final Class type) { super(ConfigItem.Module, type); this.overriding = false; } + /** + * Create item. + * + * @param module module instance + */ public ModuleItemInfoImpl(final Module module) { super(ConfigItem.Module, module); this.overriding = override.get() != null; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java index 1920500b1..24bf64867 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/DisableSupport.java @@ -15,7 +15,7 @@ public interface DisableSupport { /** * Item may be disabled either from root application class or from * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle}. For application, - * {@link io.dropwizard.Application} class stored as context and for guicey bundle actual bundle instance id + * {@link io.dropwizard.core.Application} class stored as context and for guicey bundle actual bundle instance id * is stored. * * @return contexts where item was disabled or empty collection diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/ScanSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/ScanSupport.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/ScanSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/info/sign/ScanSupport.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Option.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Option.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Option.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Option.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java index 5875f0388..b3571d5e9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/Options.java @@ -22,6 +22,11 @@ public final class Options { private final OptionsSupport optionsSupport; + /** + * Create options. + * + * @param optionsSupport options support + */ public Options(final OptionsSupport optionsSupport) { this.optionsSupport = optionsSupport; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java similarity index 98% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java index b7d8f19bb..0da29bce5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/OptionsInfo.java @@ -31,6 +31,11 @@ public final class OptionsInfo { private final OptionsSupport options; + /** + * Create info. + * + * @param support options support + */ public OptionsInfo(final OptionsSupport support) { this.options = support; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java index e8ca4b080..30261fc87 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionHolder.java @@ -16,6 +16,11 @@ public final class OptionHolder { private boolean set; private T value; + /** + * Create holder. + * + * @param option option + */ public OptionHolder(final Option option) { this.option = option; value = option.getDefaultValue(); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java index cc64ed977..b96d5977f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/internal/OptionsSupport.java @@ -21,7 +21,7 @@ * @since 09.08.2016 */ @SuppressWarnings("unchecked") -public final class OptionsSupport { +public final class OptionsSupport & Option> { private final Map options = Maps.newHashMap(); @@ -77,7 +77,7 @@ public Set getOptions() { private OptionHolder getOrCreateHolder(final T option) { OptionHolder holder = options.get(option); if (holder == null) { - holder = new OptionHolder(option); + holder = new OptionHolder<>(option); options.put(option, holder); } return holder; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionParser.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionParser.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionParser.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionParser.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java index d7f4da664..b3b577b37 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/OptionsMapper.java @@ -58,7 +58,7 @@ public class OptionsMapper { private static final String PROP_PREFIX = "prop: "; - private final Map options = new HashMap<>(); + private final Map, Object> options = new HashMap<>(); private final Set mappedProps = new HashSet<>(); private boolean print; @@ -96,7 +96,7 @@ public OptionsMapper props() { * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper props(final String prefix) { + public & Option> OptionsMapper props(final String prefix) { for (Object key : System.getProperties().keySet()) { final String name = (String) key; // don't look for directly mapped properties @@ -117,7 +117,7 @@ public OptionsMapper props(final String prefix) { * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper prop(final String name, final T option) { + public & Option> OptionsMapper prop(final String name, final T option) { return prop(name, option, null); } @@ -134,7 +134,7 @@ public OptionsMapper prop(final String name, final T o * @param value type * @return mapper instance for chained calls */ - public OptionsMapper prop(final String name, final T option, + public & Option> OptionsMapper prop(final String name, final T option, final Function converter) { mappedProps.add(name); register(PROP_PREFIX + name, option, System.getProperty(name), converter); @@ -149,7 +149,7 @@ public OptionsMapper prop(final String name, final * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper env(final String name, final T option) { + public & Option> OptionsMapper env(final String name, final T option) { return env(name, option, null); } @@ -163,7 +163,7 @@ public OptionsMapper env(final String name, final T op * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper env(final String name, final T option, + public & Option> OptionsMapper env(final String name, final T option, final Function converter) { register("env: " + name, option, System.getenv(name), converter); return this; @@ -178,7 +178,7 @@ public OptionsMapper env(final String name, final T * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper string(final T option, final String value) { + public & Option> OptionsMapper string(final T option, final String value) { return string(option, value, null); } @@ -192,7 +192,7 @@ public OptionsMapper string(final T option, final S * @param helper option type * @return mapper instance for chained calls */ - public OptionsMapper string(final T option, final String value, + public & Option> OptionsMapper string(final T option, final String value, final Function converter) { register("", option, value, converter); return this; @@ -201,13 +201,15 @@ public OptionsMapper string(final T option, final S /** * @return map of resolved options */ - public Map map() { + public Map, Object> map() { return options; } @SuppressWarnings("PMD.SystemPrintln") - private void register(final String source, final T option, final String value, - final Function converter) { + private & Option> void register(final String source, + final T option, + final String value, + final Function converter) { if (StringUtils.isNotBlank(value)) { options.put(option, converter == null ? OptionParser.parseValue(option, value) : converter.apply(value)); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java index 3cbcacade..496b1dc1d 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/option/mapper/StringConverter.java @@ -32,6 +32,14 @@ public final class StringConverter { private StringConverter() { } + /** + * Convert string to type. + * + * @param target target type + * @param value value to convert + * @param targe type + * @return converted value + */ @SuppressWarnings("unchecked") public static V convert(final Class target, final String value) { final Object res; @@ -49,7 +57,7 @@ public static V convert(final Class target, final String value) { private static V[] handleArray(final Class type, final String value) { try { return StreamSupport.stream( - Splitter.on(',').trimResults().omitEmptyStrings().split(value).spliterator(), false) + Splitter.on(',').trimResults().omitEmptyStrings().split(value).spliterator(), false) .map(val -> convertSimple(type, val)).toArray(num -> (V[]) Array.newInstance(type, num)); } catch (Exception ex) { throw new IllegalStateException("Failed to parse array " + type.getSimpleName() @@ -57,7 +65,7 @@ private static V[] handleArray(final Class type, final String value) { } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "PMD.LooseCoupling", "PMD.UnnecessaryCast"}) private static EnumSet handleEnumSet(final String value) { try { return EnumSet.copyOf((List) Arrays.asList(handleArray(Enum.class, value))); @@ -66,8 +74,7 @@ private static EnumSet handleEnumSet(final String value) { } } - @SuppressWarnings({"unchecked", "checkstyle:CyclomaticComplexity", - "PMD.NcssCount", "PMD.CyclomaticComplexity"}) + @SuppressWarnings({"unchecked", "checkstyle:CyclomaticComplexity", "PMD.CyclomaticComplexity"}) private static V convertSimple(final Class type, final String value) { Object res = null; try { @@ -106,7 +113,7 @@ private static V convertSimple(final Class type, final String value) { * @param value full enum definition * @return parsed enum */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "PMD.ExceptionAsFlowControl"}) private static Enum parseEnum(final String value) { final int idx = value.lastIndexOf('.'); try { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/DetailStat.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/DetailStat.java new file mode 100644 index 000000000..fb0257f9d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/DetailStat.java @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.module.context.stat; + +/** + * Startup time for exact item. + * + * @author Vyacheslav Rusakov + * @since 10.03.2025 + */ +public enum DetailStat { + /** + * Configuration hook run time. + */ + Hook, + /** + * Command resolution (scan), instantiation and fields injection time. + */ + Command, + /** + * Guicey bundle init time. + */ + BundleInit, + /** + * Guicey bundle run time. + */ + BundleRun, + /** + * Listeners of type processing. + */ + Listener +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java index 26c4e3523..7c6f5644f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/GuiceStatsTracker.java @@ -11,7 +11,6 @@ * Guice logs internal stats with java util logger ({@link com.google.inject.internal.util.ContinuousStopwatch}). * In order to intercept these messages, append custom handler to this logger, but just for injector creation time. */ -@SuppressWarnings("PMD.MoreThanOneLogger") public class GuiceStatsTracker { private final List messages = new ArrayList<>(); @@ -63,7 +62,7 @@ private Logger getLogger() { /** * Intercept guice stats logs. */ - private class LogsInterceptor extends Handler { + private final class LogsInterceptor extends Handler { @Override public void publish(final LogRecord logRecord) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java similarity index 77% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java index ed0cc93f0..74d8aeed2 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/Stat.java @@ -10,6 +10,11 @@ */ public enum Stat { + /** + * Timer started in time of guice bundle creation for measuring entire application startup time + * (used for measuring time since startup). + */ + OverallTime(true), /** * Overall guicey startup time (including configuration, run and jersey parts). All other timers represents this * timer detalization. @@ -19,6 +24,14 @@ public enum Stat { * Guicey time in dropwizard configuration phase. Part of {@link #GuiceyTime}. */ ConfigurationTime(true), + /** + * Time of bundle configuration. Part of {@link #ConfigurationTime} + */ + BundleBuilderTime(true), + /** + * Hooks resolution and processing time. Part of {@link #ConfigurationTime}. + */ + HooksTime(true), /** * Commands processing time. Includes environment commands members injection (always performed) * and commands registration from classpath scan (disabled by default). Part of {@link #ConfigurationTime} and @@ -56,17 +69,33 @@ public enum Stat { * phases. */ InstallersTime(true), + /** + * Installers search and instantiation time. Part of {@link #InstallersTime}. + */ + InstallersResolutionTime(true), /** * Time spent on extensions resolution (matching all extension classes with configured installers ). * Does not contain classpath scan time, because already use cached scan result (actual scan performed * before initializations). Part of {@link #InstallersTime}. */ ExtensionsRecognitionTime(true), + /** + * Guicey listeners execution time. + */ + ListenersTime(true), /** * Guicey time in dropwizard run phase (without jersey time). Part of {@link #GuiceyTime}. */ RunTime(true), + /** + * Time of configuration object parsing (to bind later by value). Part of {@link #RunTime}. + */ + ConfigurationAnalysis(true), + /** + * Time of guicey bundles run execution. Part of {@link #BundleTime} and {@link #RunTime}. + */ + GuiceyBundleRunTime(true), /** * Modules pre processing time (include Aware* interfaces processing and bindings analysis). * Also includes part of {@link #ExtensionsRecognitionTime}. @@ -96,11 +125,17 @@ public enum Stat { */ RemovedInnerModules(false), /** - * Guice SPI time of modules elements resolution. When bindings inspection is disabled with - * {@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}, this time become a part of - * overall injector creation time. + * Guice SPI time of modules elements resolution (part of {@link #ModulesProcessingTime}). When bindings + * inspection is disabled with {@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}, this stat + * will be 0, but injector creation time will increase accordingly because guice will have to do the same + * ({@link #InjectorCreationTime}). */ BindingsResolutionTime(true), + + /** + * Time of parsed bindings analysis. Part of {@link #ExtensionsRecognitionTime}. + */ + BindingsAnalysisTime(true), /** * Guice injector creation time. Part of {@link #RunTime}. */ @@ -123,7 +158,7 @@ public enum Stat { */ JerseyInstallerTime(true); - private boolean timer; + private final boolean timer; Stat(final boolean timer) { this.timer = timer; diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatTimer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatTimer.java new file mode 100644 index 000000000..b9abb2a6a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatTimer.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.module.context.stat; + +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; + +/** + * Abstraction above {@link com.google.common.base.Stopwatch} to support inlined starts + * (when something tries to start already started timer). This is required when an initialization sequence could + * change and some blocks become part of already measured scope (in this case, such a measure should be simply ignored, + * as timer already counts it). + * + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +public class StatTimer { + private final Stopwatch stopwatch; + private int started; + + /** + * Create timer. + * + * @param stopwatch watch + */ + public StatTimer(final Stopwatch stopwatch) { + this.stopwatch = stopwatch; + } + + /** + * Start timer. Timer could be started multiple times (inlined scopes), but it must be stopped accordingly. + * + * @return timer instance + */ + public StatTimer start() { + synchronized (stopwatch) { + if (started == 0) { + stopwatch.start(); + } + started++; + } + return this; + } + + /** + * Stop timer. Could be called multiple times if inlined scopes. + * + * @throws java.lang.IllegalStateException if the timer already stopped + */ + public void stop() { + Preconditions.checkState(started >= 1, "Timer already stopped"); + synchronized (stopwatch) { + if (started == 1) { + stopwatch.stop(); + } + started--; + } + } + + /** + * @return true if the timer is running + */ + public boolean isRunning() { + return stopwatch.isRunning(); + } + + /** + * @return underlying stopwatch + */ + public Stopwatch getStopwatch() { + return stopwatch; + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java similarity index 73% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java index a006cd1c0..ccce07cac 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsInfo.java @@ -3,8 +3,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; +import java.time.Duration; import java.util.List; -import java.util.concurrent.TimeUnit; +import java.util.Map; /** * Provides access to starts collected at startup. @@ -19,6 +20,11 @@ public final class StatsInfo { // have to keep full object, because stats also computed after info object creation private final StatsTracker tracker; + /** + * Create info. + * + * @param tracker tracker + */ public StatsInfo(final StatsTracker tracker) { this.tracker = tracker; } @@ -29,13 +35,22 @@ public StatsInfo(final StatsTracker tracker) { * In contrast, when using {@link #humanTime(Stat)} for such timers, correct value will be printed. * * @param name statistic name - * @return collected time in milliseconds or 0 (is stat value is not available) + * @return collected time in milliseconds or 0 (if stat value is not available) * @throws IllegalStateException if provided stat is not time stat */ public long time(final Stat name) { + return duration(name).toMillis(); + } + + /*** + * @param name statistic name + * @return collected time duration or 0 (if stat value is not available) + * @throws IllegalStateException if provided stat is not time stat + */ + public Duration duration(final Stat name) { name.requiresTimer(); final Stopwatch stopwatch = tracker.getTimers().get(name); - return stopwatch == null ? 0 : stopwatch.elapsed(TimeUnit.MILLISECONDS); + return stopwatch == null ? Duration.ZERO : stopwatch.elapsed(); } /** @@ -69,4 +84,14 @@ public int count(final Stat name) { public List getGuiceStats() { return tracker.getGuiceStats().getMessages(); } + + /** + * Detailed stats used to track duration for exact entity (command or guicey bundle). + * + * @param stat required stat + * @return all collected detailed stats of type + */ + public Map, Duration> getDetailedStats(final DetailStat stat) { + return tracker.getDetails(stat); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java similarity index 50% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java index eb11d8ab4..bd756554b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/stat/StatsTracker.java @@ -1,12 +1,18 @@ package ru.vyarus.dropwizard.guice.module.context.stat; +import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; import com.google.common.collect.Maps; +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Collectors; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.GuiceyTime; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.JerseyTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.OverallTime; /** * Internal object, used to record startup stats. Guava {#Stopwatch} used for time measurements @@ -17,9 +23,18 @@ * @since 27.07.2016 */ public final class StatsTracker { - private final Map timers = Maps.newEnumMap(Stat.class); + private final Map timers = Maps.newEnumMap(Stat.class); private final Map counters = Maps.newEnumMap(Stat.class); private final GuiceStatsTracker guiceStats = new GuiceStatsTracker(); + private final Map, Stopwatch>> detailStats = Maps.newEnumMap(DetailStat.class); + + /** + * Create tracker. + */ + public StatsTracker() { + // start measuring overall time since guice bundle creation + timer(OverallTime); + } /** * If measured first time, returns new instance. For second and following measures returns the same instance @@ -29,13 +44,35 @@ public final class StatsTracker { * @param name statistic name * @return timer to measure time */ - public Stopwatch timer(final Stat name) { - final Stopwatch watch = timers.computeIfAbsent(name, k -> Stopwatch.createUnstarted()); - // if watch was performed before then new time will sum with current + public StatTimer timer(final Stat name) { + final StatTimer watch = timers.computeIfAbsent(name, k -> new StatTimer(Stopwatch.createUnstarted())); + // if time was measured before then new time will sum with current (if timer already started then current + // start would be ignored) watch.start(); return watch; } + /** + * Stop running timer. + * + * @param name timer name + */ + public void stopTimer(final Stat name) { + Preconditions.checkNotNull(timers.get(name), "No started timer for " + name).stop(); + } + + /** + * Detail stats used to record exact entity metric (like guicey bundle init or run time). + * + * @param name detail name + * @param type target type + * @return detail timer + */ + public Stopwatch detailTimer(final DetailStat name, final Class type) { + return detailStats.computeIfAbsent(name, detailStat -> new LinkedHashMap<>()) + .computeIfAbsent(type, aClass -> Stopwatch.createUnstarted()).start(); + } + /** * Inserts value for first call and sum values for consequent calls. * @@ -80,7 +117,22 @@ public void stopJerseyTimer(final Stat name) { * @return collected timers map */ public Map getTimers() { - return timers; + return timers.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getStopwatch())); + } + + /** + * @param name detail stat name + * @return detail stats + */ + public Map, Duration> getDetails(final DetailStat name) { + final Map, Stopwatch> details = detailStats.get(name); + if (details == null) { + return Collections.emptyMap(); + } + final Map, Duration> res = new LinkedHashMap<>(); + details.forEach((type, stopwatch) -> res.put(type, stopwatch.elapsed())); + return res; } /** @@ -96,4 +148,16 @@ public Map getCounters() { public GuiceStatsTracker getGuiceStats() { return guiceStats; } + + /** + * Verify all timers stopped on application complete startup. As timers are inlinable, it is quite possible + * to not call stop enough times. + */ + public void verifyTimersDone() { + timers.get(OverallTime).stop(); + for (final Map.Entry entry : getTimers().entrySet()) { + Preconditions.checkState(!entry.getValue().isRunning(), + "Timer is still running after application startup", entry.getKey()); + } + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java similarity index 99% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java index 929519f1d..e16fcb96c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/DuplicateConfigDetector.java @@ -17,6 +17,7 @@ * @see LegacyModeDuplicatesDetector with legacy guicey behaviour implementation (always one instance per class) * @since 03.07.2019 */ +@FunctionalInterface public interface DuplicateConfigDetector { /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/LegacyModeDuplicatesDetector.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/LegacyModeDuplicatesDetector.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/LegacyModeDuplicatesDetector.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/LegacyModeDuplicatesDetector.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java index 79c01a469..8d8ecc06b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/UniqueItemsDuplicatesDetector.java @@ -17,6 +17,11 @@ public class UniqueItemsDuplicatesDetector implements DuplicateConfigDetector { private final Set items = new HashSet<>(); + /** + * Create detector. + * + * @param uniqueItems unique items + */ public UniqueItemsDuplicatesDetector(final Class... uniqueItems) { Preconditions.checkArgument(uniqueItems.length > 0, "No unique items to configured"); // use strings to correctly detect class from different class loaders (as core mechanism will correctly diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java index 6a5361a61..ec1c933e5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueDropwizardAwareModule.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.context.unique.item; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import io.dropwizard.Configuration; +import io.dropwizard.core.Configuration; import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueGuiceyBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueGuiceyBundle.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueGuiceyBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueGuiceyBundle.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueModule.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/context/unique/item/UniqueModule.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/CoreInstallersBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/CoreInstallersBundle.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/CoreInstallersBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/CoreInstallersBundle.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java index 9604c1fc9..c5933a326 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/FeatureInstaller.java @@ -1,5 +1,8 @@ package ru.vyarus.dropwizard.guice.module.installer; +import java.util.Collections; +import java.util.List; + /** * Installer serve two purposes: find extension on classpath and properly install it * (in dropwizard or somewhere else). Each installer should work with single feature. @@ -50,4 +53,15 @@ public interface FeatureInstaller { *

    Method may do nothing if reporting not required

    */ void report(); + + /** + * Method used by extensions help report + * ({@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#printExtensionsHelp()}) to show what signs this exact + * installer recognize so user could better understand extensions support specifics. + * + * @return list of extension signs installer recognize + */ + default List getRecognizableSigns() { + return Collections.singletonList(""); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java index d8e48e141..c0c650b9c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallerModule.java @@ -20,6 +20,11 @@ public class InstallerModule extends AbstractModule { private final ConfigurationContext context; + /** + * Create installer module. + * + * @param context configuration context + */ public InstallerModule(final ConfigurationContext context) { this.context = context; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java index 2b6a9bc3d..b95bd7025 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/InstallersOptions.java @@ -53,9 +53,9 @@ public enum InstallersOptions implements Option { * Startup will fail if HK2 bridge is not enabled * (see {@link ru.vyarus.dropwizard.guice.GuiceyOptions#UseHkBridge}) because without it you can't inject * any guice beans into HK2 managed instances (and if you don't need to then you don't need guice support at all). - * @deprecated in the next version HK2 support will be removed + *

    + * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed */ - @Deprecated JerseyExtensionsManagedByGuice(Boolean.class, true), /** * Force singleton scope for jersey extensions (including resources). It is highly recommended using singletons @@ -80,17 +80,27 @@ public enum InstallersOptions implements Option { *

    * When option disabled, qualifier could still be applied manually with * {@link org.glassfish.jersey.internal.inject.Custom} annotation on provider class. Or - * {@link javax.annotation.Priority} might be used instead (see {@link javax.ws.rs.Priorities} for default + * {@link jakarta.annotation.Priority} might be used instead (see {@link jakarta.ws.rs.Priorities} for default * priority constants). *

    * Option is enabled by default to mimic default dropwizard behaviour (when extensions registered manually). * But it MAY change application behaviour comparing to previous guicey versoins and so it might be desirable * to revert previous guicey behavior (and this is the main reason for option to exist). */ - PrioritizeJerseyExtensions(Boolean.class, true); + PrioritizeJerseyExtensions(Boolean.class, true), + /** + * By default, jersey extensions would be recognized by type (e.g. {@link jakarta.ws.rs.ext.ExceptionMapper}) + * (not only annotated as prvider). But, if you have complex extension hierarchies with non-abstract classes in + * the middle, classpath scan could recognize "middle" extensions and install them. Or there might be other + * situation when you need to avoid installation by type. + *

    + * When disabled, only extensions annotated with {@link jakarta.ws.rs.ext.Provider} would be recognized + * (legacy guicey behaviour). + */ + JerseyExtensionsRecognizedByType(Boolean.class, true); - private Class type; - private Object value; + private final Class type; + private final Object value; InstallersOptions(final Class type, final T value) { this.type = type; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java index a986f4816..3c68842e5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/WebInstallersBundle.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.installer; import io.dropwizard.jetty.MutableServletContextHandler; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; @@ -11,8 +11,8 @@ /** * Bundle adds servlet installers for filters, servlets and listeners installation. - * Standard java.servlet.annotation annotations ({@link javax.servlet.annotation.WebFilter}, - * {@link javax.servlet.annotation.WebServlet}, {@link javax.servlet.annotation.WebListener}) are used. + * Standard java.servlet.annotation annotations ({@link jakarta.servlet.annotation.WebFilter}, + * {@link jakarta.servlet.annotation.WebServlet}, {@link jakarta.servlet.annotation.WebListener}) are used. * Note that these annotations are not recognized by jetty automatically, because dropwizard doesn't include * jetty-annotations modules. *

    diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java new file mode 100644 index 000000000..a7f053891 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java @@ -0,0 +1,249 @@ +package ru.vyarus.dropwizard.guice.module.installer.bundle; + +import com.google.common.base.Preconditions; +import com.google.inject.Module; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; +import ru.vyarus.dropwizard.guice.module.context.option.Option; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.util.BundleSupport; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Guicey initialization object. Provides almost the same configuration methods as + * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}. Also, contains dropwizard bootstrap objects. + * May register pure dropwizard bundles. + *

    + * In contrast to main builder, guicey bundle can't: + *

      + *
    • Disable bundles (because at this stage bundles already partly processed)
    • + *
    • Use generic disable predicates (to not allow bundles disable, moreover it's tests-oriented feature)
    • + *
    • Change options (because some bundles may already apply configuration based on changed option value + * which will mean inconsistent state)
    • + *
    • Register listener, implementing {@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook} + * (because it's too late - all hooks were processed)
    • + *
    • Register some special objects like custom injector factory or custom bundles lookup
    • + *
    + * + * @author Vyacheslav Rusakov + * @since 01.08.2015 + */ +public class GuiceyBootstrap implements GuiceyCommonRegistration { + + private final ConfigurationContext context; + // path for tracking bundles installation loops (a bundle registers another bundle and so on) + // managed outside the bootstrap object, but need the reference for transitive bundles registration + private final List> bundlesPath; + private final List initOrder; + + /** + * Create bootstrap. + * @param context configuration context + * @param bundlesPath paths to track bundles installation loops + * @param initOrder holder for bundles initialization order + */ + public GuiceyBootstrap(final ConfigurationContext context, + final List> bundlesPath, + final List initOrder) { + this.context = context; + this.bundlesPath = bundlesPath; + this.initOrder = initOrder; + } + + /** + * If bundle provides new installers then they must be declared here. + * Optionally, core or other 3rd party installers may be declared also to indicate dependency + * (duplicate installers registrations will be removed). + * + * @param installers feature installer classes to register + * @return bootstrap instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#installers(Class[]) + */ + @SafeVarargs + public final GuiceyBootstrap installers(final Class... installers) { + context.registerInstallers(installers); + return this; + } + + /** + * Register other guicey bundles for installation. Bundles initialized immediately (same as transitive dropwizard + * bundles and guice modules). + *

    + * Equal instances of the same type will be considered as duplicate. + * + * @param bundles guicey bundles + * @return bootstrap instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#bundles(GuiceyBundle...) + */ + public GuiceyBootstrap bundles(final GuiceyBundle... bundles) { + // immediate registration (same as for dropwizard bundles and guice modules) + BundleSupport.initBundles(context, this, bundlesPath, initOrder, context.registerBundles(bundles)); + return this; + } + + /** + * Shortcut for dropwizard bundles registration (instead of {@code bootstrap().addBundle()}), but with + * duplicates detection and tracking in diagnostic reporting. Dropwizard bundle is immediately initialized. + * + * @param bundles dropwizard bundles to register + * @return bootstrap instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#dropwizardBundles(ConfiguredBundle...) + */ + public GuiceyBootstrap dropwizardBundles(final ConfiguredBundle... bundles) { + context.registerDropwizardBundles(bundles); + return this; + } + + /** + * @param installers feature installer types to disable + * @return bootstrap instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableInstallers(Class[]) + */ + @SafeVarargs + public final GuiceyBootstrap disableInstallers(final Class... installers) { + context.disableInstallers(installers); + return this; + } + + // ------------------------------------------------------------------ COMMON METHODS + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public Bootstrap bootstrap() { + return context.getBootstrap(); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public Application application() { + return context.getBootstrap().getApplication(); + } + + /** + * {@inheritDoc} + */ + @Override + public & Option> V option(final K option) { + return context.option(option); + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap modules(final Module... modules) { + Preconditions.checkState(modules.length > 0, "Specify at least one module"); + context.registerModules(modules); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap modulesOverride(final Module... modules) { + context.registerModulesOverride(modules); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap extensions(final Class... extensionClasses) { + context.registerExtensions(extensionClasses); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap extensionsOptional(final Class... extensionClasses) { + context.registerExtensionsOptional(extensionClasses); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap disableExtensions(final Class... extensions) { + context.disableExtensions(extensions); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + @SafeVarargs + public final GuiceyBootstrap disableModules(final Class... modules) { + context.disableModules(modules); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap listen(final GuiceyLifecycleListener... listeners) { + context.lifecycle().register(listeners); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyBootstrap shareState(final Class key, final K value) { + context.getSharedState().put(key, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public K sharedState(final Class key, final Supplier defaultValue) { + return context.getSharedState().get(key, defaultValue); + } + + /** + * {@inheritDoc} + */ + @Override + public Optional sharedState(final Class key) { + return Optional.ofNullable(context.getSharedState().get(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public K sharedStateOrFail(final Class key, final String message, final Object... args) { + return context.getSharedState().getOrFail(key, message, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void whenSharedStateReady(final Class key, final Consumer action) { + context.getSharedState().whenReady(key, action); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java similarity index 74% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java index 54554c13d..d9d0965a5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBundle.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.installer.bundle; /** - * Guicey bundle is an enhancement of dropwizard bundles ({@link io.dropwizard.ConfiguredBundle}). It allows + * Guicey bundle is an enhancement of dropwizard bundles ({@link io.dropwizard.core.ConfiguredBundle}). It allows * everything that dropwizard bundles can plus guicey specific features and so assumed to be used instead * of dropwizard bundles. But it does not mean that other dropwizard bundles can't be used: both bundle types * share the same lifecycle, so you can register dropwizard bundles from within guicey bundle (which is very important @@ -10,15 +10,15 @@ * Like dropwizard bundles, guicey bundles contains two lifecycle phases: *

      *
    • initialization - when all bundles (dropwizard and guicey) must be configured
    • - *
    • run - when dropwizard configuration and environment become available and some additional guicey + *
    • run - when dropwizard configuration and environment become available, and some additional guicey * configurations may be performed
    • *
    *

    * Extensions and guice modules may be registered (or disabled) in both phases (because guice is not yet started - * for both), but installers are registered only in initialization phase because they are used during classpath + * for both), but installers registered only in initialization phase because they are used during classpath * scan, performed on dropwizard initialization. *

    - * Bundles are extremely useful when autoscan is not used in order to group required extensions installation. + * Bundles are extremely useful when autoscan is not used to group required extensions installation. *

    * Bundle should be registered into {@link ru.vyarus.dropwizard.guice.GuiceBundle} builder. *

    @@ -26,9 +26,9 @@ * {@link ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup}. For example, it could be service loader based * lookup which automatically installs bundle when it appears in classpath. *

    - * Multiple instances of the same bundle could be registered (like with dropwizard bundles). But guicey duplicates - * mechanism will consider equal bundles as duplicate (and register only one). So in order to grant bundle - * uniqueness simply properly implement equals method or use + * Multiple instances of the same bundle could be registered (like with dropwizard bundles). But guicey deduplicates + * mechanism will consider equal bundles as duplicate (and register only one). So, to grant bundle uniqueness, + * properly implement equals method or use * {@link ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle}. See * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#duplicateConfigDetector( * ru.vyarus.dropwizard.guice.module.context.unique.DuplicateConfigDetector)} for duplicates detection mechanism info. @@ -40,31 +40,28 @@ public interface GuiceyBundle { /** * Called in initialization phase. {@link GuiceyBootstrap} contains almost the same methods as - * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}, which allows to register installers, extensions + * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}, which allows registering installers, extensions * and guice modules. Existing installer could be replaced by disabling old one and registering new. *

    * Dropwizard bundles could be also registered with - * {@link GuiceyBootstrap#dropwizardBundles(io.dropwizard.ConfiguredBundle[])} shortcut (or by directly accessing - * dropwizard bootstrap object: {@link GuiceyBootstrap#bootstrap()}. + * {@link GuiceyBootstrap#dropwizardBundles(io.dropwizard.core.ConfiguredBundle[])} shortcut (or by directly + * accessing dropwizard bootstrap object: {@link GuiceyBootstrap#bootstrap()}. *

    * As bundles could be registered only during initialization phase, it is not possible to * avoid bundle registration based on configuration (not a good practice). But, it is possible - * to use guicey options instead: for example, map option from environment variable and use to to decide if some + * to use guicey options instead: for example, map option from environment variable and use to decide if some * bundles should be activated. - *

    - * Guicey lifecycle listeners ({@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener} - * could be registered only on initialization phase - * ({@link GuiceyBootstrap#listen(ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener...)}). * * @param bootstrap guicey bootstrap object + * @throws java.lang.Exception if something goes wrong */ - default void initialize(GuiceyBootstrap bootstrap) { + default void initialize(final GuiceyBootstrap bootstrap) throws Exception { // void } /** * Called on run phase. {@link GuiceyEnvironment} contains almost the same methods as - * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}, which allows to register extensions and guice modules. + * {@link GuiceyBootstrap}, which allows registering extensions and guice modules. *

    * Direct jersey specific registrations are possible through shortcuts * {@link GuiceyEnvironment#register(Object...)} and {@link GuiceyEnvironment#register(Class[])}. @@ -79,7 +76,7 @@ default void initialize(GuiceyBootstrap bootstrap) { * @param environment guicey environment object * @throws Exception if something goes wrong */ - default void run(GuiceyEnvironment environment) throws Exception { + default void run(final GuiceyEnvironment environment) throws Exception { // void } } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyCommonRegistration.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyCommonRegistration.java new file mode 100644 index 000000000..396cb0f67 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyCommonRegistration.java @@ -0,0 +1,260 @@ +package ru.vyarus.dropwizard.guice.module.installer.bundle; + +import com.google.inject.Module; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import ru.vyarus.dropwizard.guice.module.context.option.Option; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Common methods for {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap} and + * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment} objects (common for configuration and + * run phases). + *

    + * Not implemented as base class to safe backwards compatibility (otherwise all existing bundles would have to be + * re-compiled). + * + * @param builder type + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ +public interface GuiceyCommonRegistration { + + /** + * Note: for application in run phase (when called from + * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment}), it would be too late to + * configure dropwizard bootstrap object. + * + * @param configuration type + * @return dropwizard bootstrap instance + */ + Bootstrap bootstrap(); + + /** + * Application instance may be useful for complex (half manual) integrations where access for + * injector is required. + * For example, manually registered + * {@link io.dropwizard.lifecycle.Managed} may access injector in it's start method by calling + * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup#getInjector(io.dropwizard.core.Application)}. + *

    + * NOTE: it will work in this example, because injector access will be after injector creation. + * Directly inside bundle initialization method injector could not be obtained as it's not exists yet. + * + * @param configuration type + * @return dropwizard application instance + */ + Application application(); + + /** + * Read option value. Options could be set only in application root + * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(Enum, Object)}. + * If value wasn't set there then default value will be returned. Null may return only if it was default value + * and no new value were assigned. + *

    + * Option access is tracked as option usage (all tracked data is available through + * {@link ru.vyarus.dropwizard.guice.module.context.option.OptionsInfo}). + * + * @param option option enum + * @param option value type + * @param helper type to define option + * @return assigned option value or default value + * @see ru.vyarus.dropwizard.guice.module.context.option.Option more options info + * @see ru.vyarus.dropwizard.guice.GuiceyOptions options example + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(java.lang.Enum, java.lang.Object) + * options definition + */ + & Option> V option(K option); + + /** + * Register guice modules. + *

    + * When registration called under initialization phase + * ({@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap}), neither configuration nor + * environment objects are available yet. If you need them for module, then you can wrap it with + * {@link ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule} or register modules in run phase + * (inside {@link GuiceyBundle#run(GuiceyEnvironment)}). + *

    + * When registration called under run phase + * ({@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment}), environment and configuration + * objects already available (no need to use Aware* interfaces, but if you will they will also + * work, of course). This may look like misconception because configuration appear not in configuration phase, + * but it's not: for example, in pure dropwizard you can register jersey configuration modules in run phase too. + * This brings the simplicity of use: 3rd party guice modules often require configuration values to + * be passed directly to constructor, which is impossible in initialization phase (and so you have to use Aware* + * workarounds). + * + * @param modules one or more guice modules + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modules(com.google.inject.Module...) + * @see ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule + */ + T modules(Module... modules); + + /** + * Override modules (using guice {@link com.google.inject.util.Modules#override(com.google.inject.Module...)}). + * + * @param modules overriding modules + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modulesOverride(com.google.inject.Module...) + */ + T modulesOverride(Module... modules); + + /** + * Bundle should not rely on auto-scan mechanism and so must declare all extensions manually + * (this better declares bundle content and speed ups startup). + *

    + * NOTE: startup will fail if bean not recognized by installers. Use {@link #extensionsOptional(Class[])} to + * register optional extension. + *

    + * Alternatively, you can manually bind extensions in guice module and they would be recognized + * ({@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}). + * + * @param extensionClasses extension bean classes to register + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#extensions(Class[]) + */ + T extensions(Class... extensionClasses); + + /** + * The same as {@link #extensions(Class[])}, but, in case if no installer recognize extension, will be + * automatically disabled instead of throwing error. Useful for optional extensions declaration in 3rd party + * bundles (where it is impossible to be sure what other bundles will be used and so what installers will + * be available). + *

    + * Alternatively, you can manually bind extensions in guice module and they would be recognized + * ({@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}). Extensions with no available target + * installer will simply wouldn't be detected (because installers used for recognition) and so there is no need + * to mark them as optional in this case. + * + * @param extensionClasses extension bean classes to register + * @return builder instance for chained calls + */ + T extensionsOptional(Class... extensionClasses); + + /** + * @param extensions extensions to disable (manually added, registered by bundles or with classpath scan) + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableExtensions(Class[]) + */ + T disableExtensions(Class... extensions); + + /** + * Disable both usual and overriding guice modules. + *

    + * If bindings analysis is not disabled, could also disable inner (transitive) modules, but only inside + * normal modules. + * + * @param modules guice module types to disable + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableModules(Class[]) + */ + @SuppressWarnings("unchecked") + T disableModules(Class... modules); + + /** + * Guicey broadcast a lot of events in order to indicate lifecycle phases + * ({@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle}). This could be useful + * for diagnostic logging (like {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#printLifecyclePhases()}) or + * to implement special behaviours on installers, bundles, modules extensions (listeners have access to everything). + * For example, {@link ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule} like support for guice + * modules could be implemented with listeners. + *

    + * Configuration items (modules, extensions, bundles) are not aware of each other and listeners + * could be used to tie them. For example, to tell bundle if some other bundles registered (limited + * applicability, but just for example). + *

    + * You can also use {@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter} when you need to + * handle multiple events (it replaces direct events handling with simple methods). + *

    + * Listener is not registered if equal listener were already registered ({@link java.util.Set} used as + * listeners storage), so if you need to be sure that only one instance of some listener will be used + * implement {@link Object#equals(Object)} and {@link Object#hashCode()}. + * + * @param listeners guicey lifecycle listeners + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle + * @see ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter + * @see ru.vyarus.dropwizard.guice.module.lifecycle.UniqueGuiceyLifecycleListener + */ + T listen(GuiceyLifecycleListener... listeners); + + /** + * Share global state to be used in other bundles (during configuration). This was added for very special cases + * when shared state is unavoidable (to not re-invent the wheel each time)! + *

    + * During application strartup, shared state could be requested with a static call + * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#getStartupInstance()}, but only + * from main thread. + *

    + * Internally, state is linked to application instance, so it would be safe to use with concurrent tests. + * Value could be accessed statically with application instance: + * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#lookup(Application, Class)}. + *

    + * In some cases, it is preferred to use bundle class as key. Value could be set only once + * (to prevent hard to track situations). + *

    + * If initialization point could vary (first access should initialize it) use + * {@link #sharedState(Class, java.util.function.Supplier)} instead. + * + * @param key shared object key + * @param value shared object + * @param shared object type + * @return builder instance for chained calls + * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + */ + T shareState(Class key, K value); + + /** + * Alternative shared value initialization for cases when first accessed bundle should init state value + * and all others just use it. + *

    + * It is preferred to initialize shared state under initialization phase to avoid problems related to + * initialization order (assuming state is used under run phase). But in some cases, it is not possible. + * + * @param key shared object key + * @param defaultValue default object provider + * @param shared object type + * @return shared object (possibly just created) + * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + */ + K sharedState(Class key, Supplier defaultValue); + + /** + * Access shared value. + * + * @param key shared object key + * @param shared object type + * @return shared object + * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + */ + Optional sharedState(Class key); + + /** + * Used to access shared state value and immediately fail if value not yet set (most likely due to incorrect + * configuration order). + * + * @param key shared object key + * @param message exception message (could use {@link String#format(String, Object...)} placeholders) + * @param args placeholder arguments for error message + * @param shared object type + * @return shared object + * @throws IllegalStateException if no value available + * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + */ + K sharedStateOrFail(Class key, String message, Object... args); + + /** + * Reactive shared value access: if value already available action called immediately, otherwise action would + * be called when value set (note that value could be set only once). + * + * @param key shared object key + * @param action action to execute when value would be set + * @param value type + */ + void whenSharedStateReady(Class key, Consumer action); +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java similarity index 58% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java index 02bca0e04..6564e50ed 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyEnvironment.java @@ -2,14 +2,18 @@ import com.google.common.base.Preconditions; import com.google.inject.Module; -import io.dropwizard.Application; -import io.dropwizard.Configuration; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import io.dropwizard.lifecycle.Managed; import io.dropwizard.lifecycle.ServerLifecycleListener; -import io.dropwizard.setup.Environment; import org.eclipse.jetty.util.component.LifeCycle; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.option.Option; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationShutdownListener; +import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationShutdownListenerAdapter; import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationStartupListener; import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.ApplicationStartupListenerAdapter; import ru.vyarus.dropwizard.guice.module.installer.bundle.listener.GuiceyStartupListener; @@ -18,8 +22,10 @@ import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -32,11 +38,16 @@ * @author Vyacheslav Rusakov * @since 13.06.2019 */ -@SuppressWarnings("PMD.TooManyMethods") -public class GuiceyEnvironment { +@SuppressWarnings({"PMD.TooManyMethods", "ClassFanOutComplexity", "PMD.CouplingBetweenObjects"}) +public class GuiceyEnvironment implements GuiceyCommonRegistration { private final ConfigurationContext context; + /** + * Create environment. + * + * @param context configuration context + */ public GuiceyEnvironment(final ConfigurationContext context) { this.context = context; } @@ -116,6 +127,41 @@ public List configurations(final Class type) { return configurationTree().valuesByType(type); } + /** + * Search for exactly one annotated configuration value. It is not possible to provide the exact annotation + * instance, but you can create a class implementing annotation and use it for search. For example, guice + * {@link com.google.inject.name.Named} annotation has {@link com.google.inject.name.Names#named(String)}: + * it is important that real annotation instance and "pseudo" annotation object would be equal. + *

    + * For annotations without attributes use annotation type: {@link #annotatedConfiguration(Class)}. + *

    + * For multiple values use {@code configurationTree().annotatedValues()}. + * + * @param annotation annotation instance (equal object) to search for an annotated config path + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + */ + protected T annotatedConfiguration(final Annotation annotation) { + return configurationTree().annotatedValue(annotation); + } + + /** + * Search for exactly one configuration value with qualifier annotation (without attributes). For cases when + * annotation with attributes used - use {@link #annotatedConfiguration(java.lang.annotation.Annotation)} + * (current method would search only by annotation type, ignoring any (possible) attributes). + *

    + * For multiple values use {@code configurationTree().annotatedValues()}. + * + * @param qualifierType qualifier annotation type + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + */ + protected T annotatedConfiguration(final Class qualifierType) { + return configurationTree().annotatedValue(qualifierType); + } + /** * Raw configuration introspection info. Could be used for more sophisticated configuration searches then * provided in shortcut methods. @@ -141,103 +187,6 @@ public Environment environment() { return context.getEnvironment(); } - /** - * Application instance may be useful for complex (half manual) integrations where access for - * injector is required. - * For example, manually registered - * {@link io.dropwizard.lifecycle.Managed} may access injector in it's start method by calling - * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup#getInjector(Application)}. - *

    - * NOTE: it will work in this example, because injector access will be after injector creation. - * Directly inside bundle initialization method injector could not be obtained as it's not exists yet. - * - * @return dropwizard application instance - */ - public Application application() { - return context.getBootstrap().getApplication(); - } - - /** - * Read option value. Options could be set only in application root - * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(Enum, Object)}. - * If value wasn't set there then default value will be returned. Null may return only if it was default value - * and no new value were assigned. - *

    - * Option access is tracked as option usage (all tracked data is available through - * {@link ru.vyarus.dropwizard.guice.module.context.option.OptionsInfo}). - * - * @param option option enum - * @param option value type - * @param helper type to define option - * @return assigned option value or default value - * @see Option more options info - * @see ru.vyarus.dropwizard.guice.GuiceyOptions options example - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(java.lang.Enum, java.lang.Object) - * options definition - */ - public V option(final T option) { - return context.option(option); - } - - /** - * Register guice modules. - *

    - * Note that this registration appear in run phase and so you already have access - * to environment and configuration (and don't need to use Aware* interfaces, but if you will they will also - * work, of course). This may look like misconception because configuration appear not in configuration phase, - * but it's not: for example, in pure dropwizard you can register jersey configuration modules in run phase too. - * This brings the simplicity of use: 3rd party guice modules often require configuration values to - * be passed directly to constructor, which is impossible in initialization phase (and so you have to use Aware* - * workarounds). - * - * @param modules one or more guice modules - * @return environment instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modules(com.google.inject.Module...) - */ - public GuiceyEnvironment modules(final Module... modules) { - Preconditions.checkState(modules.length > 0, "Specify at least one module"); - context.registerModules(modules); - return this; - } - - /** - * Override modules (using guice {@link com.google.inject.util.Modules#override(Module...)}). - * - * @param modules overriding modules - * @return environment instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modulesOverride(Module...) - */ - public GuiceyEnvironment modulesOverride(final Module... modules) { - context.registerModulesOverride(modules); - return this; - } - - /** - * @param extensions extensions to disable (manually added, registered by bundles or with classpath scan) - * @return environment instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableExtensions(Class[]) - */ - public final GuiceyEnvironment disableExtensions(final Class... extensions) { - context.disableExtensions(extensions); - return this; - } - - /** - * Disable both usual and overriding guice modules. - *

    - * If bindings analysis is not disabled, could also disable inner (transitive) modules, but only inside - * normal modules. - * - * @param modules guice module types to disable - * @return environment instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableModules(Class[]) - */ - @SafeVarargs - public final GuiceyEnvironment disableModules(final Class... modules) { - context.disableModules(modules); - return this; - } - /** * Shortcut for {@code environment().jersey().register()} for direct registration of jersey extensions. * For the most cases prefer automatic installation of jersey extensions with guicey installer. @@ -280,35 +229,71 @@ public GuiceyEnvironment manage(final Managed managed) { } /** - * Guicey broadcast a lot of events in order to indicate lifecycle phases - * ({@linkplain ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle}). Listener, registered in run phase - * could listen events from {@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle#BundlesStarted}. + * Code to execute after guice injector creation (but still under run phase). May be used for manual + * configurations (registrations into dropwizard environment). *

    - * Listener is not registered if equal listener was already registered ({@link java.util.Set} used as - * listeners storage), so if you need to be sure that only one instance of some listener will be used - * implement {@link Object#equals(Object)}. + * Listener will be called on environment command start too. + *

    + * Note: there is no registration method for this listener in main guice bundle builder + * ({@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}) because it is assumed, that such blocks would + * always be wrapped with bundles to improve application readability. * - * @param listeners guicey lifecycle listeners - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#listen(GuiceyLifecycleListener...) + * @param listener listener to call after injector creation + * @param configuration type + * @return builder instance for chained calls */ - public GuiceyEnvironment listen(final GuiceyLifecycleListener... listeners) { - context.lifecycle().register(listeners); - return this; + public GuiceyEnvironment onGuiceyStartup(final GuiceyStartupListener listener) { + return listen(new GuiceyStartupListenerAdapter<>(listener)); + } + + /** + * Code to execute after complete application startup. For server command it would happen after jetty startup + * and for lightweight guicey test helpers ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}) - after + * guicey start (as jetty not started in this case). In both cases, application completely started at this moment. + * Suitable for reporting. + *

    + * If you need to listen only for real server startup then use + * {@link #listenServer(io.dropwizard.lifecycle.ServerLifecycleListener)} instead. + *

    + * Not called on custom command execution (because no lifecycle involved in this case). In this case you can use + * {@link #onGuiceyStartup(GuiceyStartupListener)} as always executed point. + * + * @param listener listener to call on server startup + * @return builder instance for chained calls + */ + public GuiceyEnvironment onApplicationStartup(final ApplicationStartupListener listener) { + return listen(new ApplicationStartupListenerAdapter(listener)); } /** - * Shortcut for {@link ServerLifecycleListener} registration. + * Code to execute after complete application shutdown. Called not only for real application but for + * environment commands and lightweight guicey test helpers + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}). Suitable for closing additional resources. + *

    + * If you need to listen only for real server shutdown then use + * {@link #listenServer(io.dropwizard.lifecycle.ServerLifecycleListener)} instead. + *

    + * Not called on command execution because no lifecycle involved in this case. + * + * @param listener listener to call on server startup + * @return builder instance for chained calls + */ + public GuiceyEnvironment onApplicationShutdown(final ApplicationShutdownListener listener) { + return listen(new ApplicationShutdownListenerAdapter(listener)); + } + + /** + * Shortcut for {@code environment().lifecycle().addServerLifecycleListener} registration. *

    * Note that server listener is called only when jetty starts up and so will not be called with lightweight * guicey test helpers {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}. Prefer using * {@link #onApplicationStartup(ApplicationStartupListener)} to be correctly called in tests (of course, if not - * server only execution is desired). + * server-only execution is desired). *

    - * Obviously not called for custom command execution. + * Not called for custom command execution. * * @param listener server startup listener. - * @return environment instance for chained calls + * @return builder instance for chained calls */ public GuiceyEnvironment listenServer(final ServerLifecycleListener listener) { environment().lifecycle().addServerLifecycleListener(listener); @@ -316,7 +301,8 @@ public GuiceyEnvironment listenServer(final ServerLifecycleListener listener) { } /** - * Shortcut for jetty lifecycle listener {@link LifeCycle.Listener listener} registration. + * Shortcut for jetty lifecycle listener {@code environment().lifecycle().addEventListener(listener)} + * registration. *

    * Lifecycle listeners are called with lightweight guicey test helpers * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp} which makes them perfectly suitable for reporting. @@ -327,123 +313,158 @@ public GuiceyEnvironment listenServer(final ServerLifecycleListener listener) { * Listeners are not called on custom command execution. * * @param listener jetty - * @return environment instance for chained calls + * @return builder instance for chained calls */ public GuiceyEnvironment listenJetty(final LifeCycle.Listener listener) { - environment().lifecycle().addLifeCycleListener(listener); + environment().lifecycle().addEventListener(listener); return this; } /** - * Share global state to be used in other bundles (during configuration). This was added for very special cases - * when shared state is unavoidable (to not re-invent the wheel each time)! - *

    - * It is preferred to initialize shared state under initialization phase to avoid problems related to - * initialization order (assuming state is used under run phase). But, in some cases, it is not possible. + * Shortcut for jetty events and requests listener {@code environment().jersey().register(listener)} + * registration. *

    - * Internally, state is linked to application instance, so it would be safe to use with concurrent tests. - * Value could be accessed statically with application instance: - * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#lookup(Application, Class)}. - *

    - * During application strartup, shared state could be requested with a static call - * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#getStartupInstance()}, but only - * from main thread. - *

    - * In some cases, it is preferred to use bundle class as key. Value could be set only once - * (to prevent hard to track situations). - *

    - * If initialization point could vary (first access should initialize it) use - * {@link #sharedState(Class, java.util.function.Supplier)} instead. + * Listeners are not called on custom command execution. * - * @param key shared object key - * @param value shared object - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + * @param listener listener instance + * @return builder instance for chained calls */ - public GuiceyEnvironment shareState(final Class key, final Object value) { - context.getSharedState().put(key, value); + public GuiceyEnvironment listenJersey(final ApplicationEventListener listener) { + environment().jersey().register(listener); return this; } + // ------------------------------------------------------------------ COMMON METHODS + /** - * Alternative shared value initialization for cases when first accessed bundle should init state value - * and all other just use it. - *

    - * It is preferred to initialize shared state under initialization phase to avoid problems related to - * initialization order (assuming state is used under run phase). But, in some cases, it is not possible. - * - * @param key shared object key - * @param defaultValue default object provider - * @param shared object type - * @return shared object (possibly just created) - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + * {@inheritDoc} */ - public T sharedState(final Class key, final Supplier defaultValue) { - return context.getSharedState().get(key, defaultValue); + @Override + @SuppressWarnings("unchecked") + public Bootstrap bootstrap() { + return context.getBootstrap(); } /** - * Access shared value. - * - * @param key shared object key - * @param shared object type - * @return shared object - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState + * {@inheritDoc} */ - public Optional sharedState(final Class key) { - return Optional.ofNullable(context.getSharedState().get(key)); + @Override + @SuppressWarnings("unchecked") + public Application application() { + return context.getBootstrap().getApplication(); } /** - * Used to access shared state value and immediately fail if value not yet set (most likely due to incorrect - * configuration order). - * - * @param key shared object key - * @param message exception message (could use {@link String#format(String, Object...)} placeholders) - * @param args placeholder arguments for error message - * @param shared object type - * @return shared object - * @throws IllegalStateException if not value available - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState - */ - public T sharedStateOrFail(final Class key, final String message, final Object... args) { - return context.getSharedState().getOrFail(key, message, args); + * {@inheritDoc} + */ + @Override + public & Option> V option(final K option) { + return context.option(option); } /** - * Code to execute after guice injector creation (but still under run phase). May be used for manual - * configurations (registrations into dropwizard environment). - *

    - * Listener will be called on environment command start too. - *

    - * Note: there is no registration method for this listener in main guice bundle builder - * ({@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}) because it is assumed, that such blocks would - * always be wrapped with bundles to improve application readability. - * - * @param listener listener to call after injector creation - * @return environment instance for chained calls + * {@inheritDoc} */ - public GuiceyEnvironment onGuiceyStartup(final GuiceyStartupListener listener) { - return listen(new GuiceyStartupListenerAdapter(listener)); + @Override + public GuiceyEnvironment modules(final Module... modules) { + Preconditions.checkState(modules.length > 0, "Specify at least one module"); + context.registerModules(modules); + return this; } /** - * Code to execute after complete application startup. For server command it would happen after jetty startup - * and for lightweight guicey test helpers ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}) - after - * guicey start (as jetty not started in this case). In both cases, application completely started at this moment. - * Suitable for reporting. - *

    - * If you need to listen only for real server startup then use {@link #listenServer(ServerLifecycleListener)} - * instead. - *

    - * Not called on custom command execution (because no lifecycle involved in this case). In this case you can use - * {@link #onGuiceyStartup(GuiceyStartupListener)} as always executed point. - * - * @param listener listener to call on server startup - * @return environment instance for chained calls + * {@inheritDoc} */ - public GuiceyEnvironment onApplicationStartup(final ApplicationStartupListener listener) { - return listen(new ApplicationStartupListenerAdapter(listener)); + @Override + public GuiceyEnvironment modulesOverride(final Module... modules) { + context.registerModulesOverride(modules); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyEnvironment extensions(final Class... extensionClasses) { + context.registerExtensions(extensionClasses); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyEnvironment extensionsOptional(final Class... extensionClasses) { + context.registerExtensionsOptional(extensionClasses); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyEnvironment disableExtensions(final Class... extensions) { + context.disableExtensions(extensions); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + @SafeVarargs + public final GuiceyEnvironment disableModules(final Class... modules) { + context.disableModules(modules); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyEnvironment listen(final GuiceyLifecycleListener... listeners) { + context.lifecycle().register(listeners); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GuiceyEnvironment shareState(final Class key, final K value) { + context.getSharedState().put(key, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public K sharedState(final Class key, final Supplier defaultValue) { + return context.getSharedState().get(key, defaultValue); } + /** + * {@inheritDoc} + */ + @Override + public Optional sharedState(final Class key) { + return Optional.ofNullable(context.getSharedState().get(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public K sharedStateOrFail(final Class key, final String message, final Object... args) { + return context.getSharedState().getOrFail(key, message, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void whenSharedStateReady(final Class key, final Consumer action) { + context.getSharedState().whenReady(key, action); + } } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListener.java new file mode 100644 index 000000000..ddfc2661c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListener.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.module.installer.bundle.listener; + +import com.google.inject.Injector; + +/** + * Dropwizard application shut down listener. Useful for an additional shutdown logic. Supposed to be used instead of + * {@link io.dropwizard.lifecycle.ServerLifecycleListener} (because server listener is not called for guicey + * lightweight tests) and instead of {@link org.eclipse.jetty.util.component.LifeCycle.Listener} in cases when only + * shutdown event is important (easier to use). + * + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ +@FunctionalInterface +public interface ApplicationShutdownListener { + + /** + * Called after server shutdown (including shutdown after lightweight guicey tests). + *

    + * Only an injector is provided because all other objects could be obtained from it, if required. + * + * @param injector guice injector + */ + void stopped(Injector injector); +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListenerAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListenerAdapter.java new file mode 100644 index 000000000..b765df2b2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationShutdownListenerAdapter.java @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.module.installer.bundle.listener; + +import com.google.common.base.Throwables; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; + +/** + * {@link ApplicationShutdownListener} adapter for guicey lifecycle. + * + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ +public class ApplicationShutdownListenerAdapter extends GuiceyLifecycleAdapter { + + private final ApplicationShutdownListener listener; + + /** + * Create adapter. + * + * @param listener listener + */ + public ApplicationShutdownListenerAdapter(final ApplicationShutdownListener listener) { + this.listener = listener; + } + + @Override + protected void applicationStopped(final ApplicationStoppedEvent event) { + try { + listener.stopped(event.getInjector()); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new IllegalStateException("Failed to process startup listener", e); + } + } + + @Override + public String toString() { + return "ShutdownListener(" + listener.getClass().getSimpleName() + ")"; + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java similarity index 68% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java index fc370b60f..81754d710 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListener.java @@ -1,13 +1,13 @@ package ru.vyarus.dropwizard.guice.module.installer.bundle.listener; import com.google.inject.Injector; -import io.dropwizard.lifecycle.ServerLifecycleListener; /** * Dropwizard application complete startup listener. Useful for delayed code execution after server startup. - * Supposed to be used instead of {@link ServerLifecycleListener} (because server listener is not called for guicey - * lightweight tests) and instead of {@link org.eclipse.jetty.util.component.LifeCycle.Listener} in cases when only - * startup event is important (easier to use). + * Supposed to be used instead of {@link io.dropwizard.lifecycle.ServerLifecycleListener} (because server listener + * is not called for guicey lightweight tests) and instead of + * {@link org.eclipse.jetty.util.component.LifeCycle.Listener} in cases when only startup event is important + * (easier to use). *

    * It also receives application injector to simplify usage. * diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java index e15f2f177..f41b02da8 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/ApplicationStartupListenerAdapter.java @@ -13,6 +13,11 @@ public class ApplicationStartupListenerAdapter extends GuiceyLifecycleAdapter { private final ApplicationStartupListener listener; + /** + * Create adapter. + * + * @param listener listener + */ public ApplicationStartupListenerAdapter(final ApplicationStartupListener listener) { this.listener = listener; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java index be3e97352..76991465d 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListener.java @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.module.installer.bundle.listener; import com.google.inject.Injector; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; /** * Called after guicey startup (after {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(Configuration, Environment)} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java similarity index 75% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java index b1da1f967..fb8e113f0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/listener/GuiceyStartupListenerAdapter.java @@ -1,24 +1,30 @@ package ru.vyarus.dropwizard.guice.module.installer.bundle.listener; import com.google.common.base.Throwables; +import io.dropwizard.core.Configuration; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; import ru.vyarus.dropwizard.guice.module.lifecycle.event.run.ApplicationRunEvent; /** * {@link GuiceyStartupListener} adapter for guicey lifecycle. * + * @param configuration type * @author Vyacheslav Rusakov * @since 28.09.2019 */ -public class GuiceyStartupListenerAdapter extends GuiceyLifecycleAdapter { - private final GuiceyStartupListener listener; +public class GuiceyStartupListenerAdapter extends GuiceyLifecycleAdapter { + private final GuiceyStartupListener listener; - public GuiceyStartupListenerAdapter(final GuiceyStartupListener listener) { + /** + * Create adapter. + * + * @param listener listener + */ + public GuiceyStartupListenerAdapter(final GuiceyStartupListener listener) { this.listener = listener; } @Override - @SuppressWarnings("unchecked") protected void applicationRun(final ApplicationRunEvent event) { try { listener.configure(event.getConfiguration(), event.getEnvironment(), event.getInjector()); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java index 08f76c827..ca97084d6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/LifeCycleInstaller.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.installer.feature; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import org.eclipse.jetty.util.component.LifeCycle; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; @@ -10,6 +10,9 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; +import java.util.Collections; +import java.util.List; + /** * Lifecycle objects installer. * Looks for classes implementing {@code org.eclipse.jetty.util.component.LifeCycle} and register them in environment. @@ -37,4 +40,9 @@ public void install(final Environment environment, final LifeCycle instance) { public void report() { reporter.report(); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("implements " + LifeCycle.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java index c44dcc591..c97b51cf3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/ManagedInstaller.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.installer.feature; import io.dropwizard.lifecycle.Managed; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; @@ -10,6 +10,9 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; +import java.util.Collections; +import java.util.List; + /** * Managed objects installer. * Looks for classes implementing {@code io.dropwizard.lifecycle.Managed} and register them in environment. @@ -37,4 +40,9 @@ public void install(final Environment environment, final Managed instance) { public void report() { reporter.report(); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("implements " + Managed.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java similarity index 80% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java index 0bf05eb6e..684c80248 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/TaskInstaller.java @@ -1,12 +1,15 @@ package ru.vyarus.dropwizard.guice.module.installer.feature; import io.dropwizard.servlets.tasks.Task; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; import ru.vyarus.dropwizard.guice.module.installer.order.Order; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import java.util.Collections; +import java.util.List; + /** * Dropwizard tasks installer. * Looks for classes extending {@code io.dropwizard.servlets.tasks.Task} and register in environment. @@ -31,4 +34,9 @@ public void install(final Environment environment, final Task instance) { public void report() { // dropwizard logs installed tasks } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("extends " + Task.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingleton.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingleton.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingleton.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingleton.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java index e7b45db02..0dbc5a128 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/eager/EagerSingletonInstaller.java @@ -14,9 +14,11 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import javax.inject.Singleton; +import jakarta.inject.Singleton; import java.lang.annotation.Annotation; +import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; /** @@ -52,12 +54,12 @@ public void bind(final Binder binder, final Class type, final boolean lazy) { public void manualBinding(final Binder binder, final Class type, final Binding binding) { // we can only validate existing binding here (actually entire extension is pretty useless in case of manual // binding) - final Class scope = binding.acceptScopingVisitor(VISITOR); - // in production all services will work as eager singletons, for report (TOOL stage) consider also valid + final Class scope = VISITOR.performDetection(binding); + // in production, all services will work as eager singletons, for report (TOOL stage) consider also valid Preconditions.checkArgument(scope.equals(EagerSingleton.class) || (!binder.currentStage().equals(Stage.DEVELOPMENT) && scope.equals(Singleton.class)), - // intentially no "at" before stacktrtace because idea may hide error in some cases + // intentionally no "at" before stacktrtace because idea may hide error in some cases "Eager bean, declared manually is not marked .asEagerSingleton(): %s (%s)", type.getName(), BindingUtils.getDeclarationSource(binding)); } @@ -78,4 +80,9 @@ public void report() { prerender.clear(); reporter.report(); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("@" + EagerSingleton.class.getSimpleName() + " on class"); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java index f808d3953..9881ebd40 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/HealthCheckInstaller.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.installer.feature.health; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; @@ -8,6 +8,9 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; +import java.util.Collections; +import java.util.List; + /** * Health check installer. * Looks for classes extending @@ -38,4 +41,9 @@ public void install(final Environment environment, final NamedHealthCheck instan public void report() { reporter.report(); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("extends " + NamedHealthCheck.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/NamedHealthCheck.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/NamedHealthCheck.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/NamedHealthCheck.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/health/NamedHealthCheck.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java index e84c93fcb..483baa136 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/AbstractJerseyInstaller.java @@ -1,18 +1,15 @@ package ru.vyarus.dropwizard.guice.module.installer.feature.jersey; import com.google.inject.Binder; -import com.google.inject.ScopeAnnotation; import com.google.inject.binder.AnnotatedBindingBuilder; +import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding; import ru.vyarus.dropwizard.guice.module.installer.option.InstallerOptionsSupport; - -import javax.inject.Scope; -import javax.inject.Singleton; -import java.lang.annotation.Annotation; +import ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils; import static ru.vyarus.dropwizard.guice.module.installer.InstallersOptions.ForceSingletonForJerseyExtensions; import static ru.vyarus.dropwizard.guice.module.installer.InstallersOptions.JerseyExtensionsManagedByGuice; @@ -29,6 +26,9 @@ public abstract class AbstractJerseyInstaller extends InstallerOptionsSupport FeatureInstaller, JerseyInstaller { + /** + * Shared logger. + */ protected final Logger logger = LoggerFactory.getLogger(getClass()); /** @@ -92,19 +92,6 @@ protected boolean isForceSingleton(final Class type, final boolean hkManaged) * @return true if scope annotation found, false otherwise */ private boolean hasScopeAnnotation(final Class type, final boolean hkManaged) { - boolean found = false; - for (Annotation ann : type.getAnnotations()) { - final Class annType = ann.annotationType(); - if (annType.isAnnotationPresent(Scope.class)) { - found = true; - break; - } - // guice has special marker annotation - if (!hkManaged && annType.isAnnotationPresent(ScopeAnnotation.class)) { - found = true; - break; - } - } - return found; + return BindingUtils.findScopingAnnotation(type, !hkManaged) != null; } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java similarity index 88% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java index f8a962e71..80b2c990c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/GuiceManaged.java @@ -10,14 +10,14 @@ * extensions are already managed by guice and so annotation is useless. But when hk-first mode enabled * (see {@link ru.vyarus.dropwizard.guice.module.installer.InstallersOptions#JerseyExtensionsManagedByGuice}) then * annotation could be used to mark exceptional beans which still must be managed by guice. + *

    + * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @author Vyacheslav Rusakov * @see JerseyManaged * @since 28.04.2018 - * @deprecated in the next version HK2 support will be removed and annotation will become useless */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Deprecated public @interface GuiceManaged { } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java similarity index 82% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java index c39272642..daa4ba8eb 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyFeatureInstaller.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.installer.feature.jersey; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; @@ -8,7 +8,9 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import javax.ws.rs.core.Feature; +import jakarta.ws.rs.core.Feature; +import java.util.Collections; +import java.util.List; /** * Jersey feature installer. @@ -39,4 +41,9 @@ public void install(final Environment environment, final Feature instance) { reporter.line(RenderUtils.renderClassLine(FeatureUtils.getInstanceClass(instance))); environment.jersey().register(instance); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("implements " + Feature.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java index 4784b6595..1b77fd2e3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/JerseyManaged.java @@ -13,7 +13,7 @@ *

    * Guice context is started before HK2, but HK2 related bindings (using service locator instance) will appear * in guice context only after HK2 context creation. So if bean directly depends on HK2 services - * (dependencies can't be wrapped with {@link javax.inject.Provider}, there is no way to properly create + * (dependencies can't be wrapped with {@link jakarta.inject.Provider}, there is no way to properly create * it in guice context. *

    * Good examples for this are {@link org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider} @@ -22,7 +22,7 @@ *

    * Still guice bindings could be used in HK2 managed bean (especially other extensions, installed by * {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller}. - * In case of problems with lifecycle, simply use {@link javax.inject.Provider} to wrap actual binding and + * In case of problems with lifecycle, simply use {@link jakarta.inject.Provider} to wrap actual binding and * delay it's resolution. *

    * In fact, using this annotation is the same as registering bean directly in jersey. Installer just @@ -30,15 +30,15 @@ *

    * Annotation will do nothing if HK2-first mode enabled: * {@link ru.vyarus.dropwizard.guice.module.installer.InstallersOptions#JerseyExtensionsManagedByGuice} + *

    + * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @author Vyacheslav Rusakov * @see ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding as alternative solution * @see GuiceManaged * @since 21.11.2014 - * @deprecated in the next version HK2 support will be removed and annotation will become useless */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Deprecated public @interface JerseyManaged { } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java index 7bba07280..7d0462b7f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/ResourceInstaller.java @@ -5,7 +5,7 @@ import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.Injector; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import org.glassfish.jersey.internal.inject.AbstractBinder; import ru.vyarus.dropwizard.guice.module.installer.install.TypeInstaller; import ru.vyarus.dropwizard.guice.module.installer.install.binding.BindingInstaller; @@ -14,7 +14,9 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding; -import javax.ws.rs.Path; +import jakarta.ws.rs.Path; +import java.util.Arrays; +import java.util.List; /** * Jersey resource installer. @@ -92,4 +94,10 @@ private boolean hasMatchedInterfaces(final Class type) { } return matches; } + + @Override + public List getRecognizableSigns() { + return Arrays.asList("@" + Path.class.getSimpleName() + " on class", + "@" + Path.class.getSimpleName() + " on implemented interface"); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java similarity index 78% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java index 81ff25840..fdee24294 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/JerseyProviderInstaller.java @@ -9,6 +9,7 @@ import com.google.inject.Stage; import org.glassfish.jersey.internal.inject.AbstractBinder; import org.glassfish.jersey.internal.inject.InjectionResolver; +import org.glassfish.jersey.server.model.ModelProcessor; import org.glassfish.jersey.server.monitoring.ApplicationEventListener; import org.glassfish.jersey.server.spi.internal.ValueParamProvider; import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions; @@ -20,10 +21,13 @@ import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.java.generics.resolver.GenericsResolver; -import javax.ws.rs.container.ContainerRequestFilter; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.container.DynamicFeature; -import javax.ws.rs.ext.*; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.ext.*; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.function.Supplier; @@ -32,7 +36,12 @@ /** * Jersey provider installer. - * Looks for classes annotated with {@link javax.ws.rs.ext.Provider} and register bindings in HK context. + * Looks for jersey extension classes and classes annotated with {@link jakarta.ws.rs.ext.Provider} and register + * bindings in HK context. + *

    + * Registration by extension type might be disabled using + * {@link ru.vyarus.dropwizard.guice.module.installer.InstallersOptions#JerseyExtensionsRecognizedByType} option + * (for legacy behaviour - register classed only annotated with {@link jakarta.ws.rs.ext.Provider}). *

    * By default, user providers are prioritized (with {@link org.glassfish.jersey.internal.inject.Custom} * qualifier). This is the default dropwizard behaviour for direct provider registration with @@ -44,7 +53,7 @@ * guicey versions behaviour). When auto prioritization disabled, {@link org.glassfish.jersey.internal.inject.Custom} * annotation may be used directly (to prioritize exact providers). *

    - * {@link javax.annotation.Priority} may be used to order providers (see {@link javax.ws.rs.Priorities} for + * {@link jakarta.annotation.Priority} may be used to order providers (see {@link jakarta.ws.rs.Priorities} for * the default priority constants). *

    * If provider is annotated with {@link JerseyManaged} it's instance will be created by HK2, not guice. @@ -65,6 +74,7 @@ * @see ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding * @since 10.10.2014 */ +@SuppressWarnings("PMD.ExcessiveImports") @Order(30) public class JerseyProviderInstaller extends AbstractJerseyInstaller implements BindingInstaller { @@ -82,14 +92,18 @@ public class JerseyProviderInstaller extends AbstractJerseyInstaller imp DynamicFeature.class, ValueParamProvider.class, InjectionResolver.class, - ApplicationEventListener.class + ApplicationEventListener.class, + ModelProcessor.class ); private final ProviderReporter reporter = new ProviderReporter(); @Override public boolean matches(final Class type) { - return FeatureUtils.hasAnnotation(type, Provider.class); + return FeatureUtils.hasAnnotation(type, Provider.class) + || (!Modifier.isAbstract(type.getModifiers()) + && (boolean) option(InstallersOptions.JerseyExtensionsRecognizedByType) + && EXTENSION_TYPES.stream().anyMatch(ext -> ext.isAssignableFrom(type))); } @Override @@ -137,7 +151,9 @@ public void install(final AbstractBinder binder, final Injector injector, final GenericsResolver.resolve(type).getGenericsInfo().getComposingTypes()); if (!extensions.isEmpty()) { for (Class ext : extensions) { - bindSpecificComponent(binder, injector, type, ext, hkExtension, forceSingleton, prioritize); + bindSpecificComponent(binder, injector, type, ext, hkExtension, forceSingleton, prioritize, + // model processor must be bound by instance (initialization specific) + ModelProcessor.class.equals(ext)); } } else { // no known extension found @@ -150,4 +166,16 @@ public void install(final AbstractBinder binder, final Injector injector, final public void report() { reporter.report(); } + + @Override + public List getRecognizableSigns() { + final List res = new ArrayList<>(); + res.add("@" + Provider.class.getSimpleName() + " on class"); + if (option(InstallersOptions.JerseyExtensionsRecognizedByType)) { + for (Class ext : EXTENSION_TYPES) { + res.add("implements " + ext.getSimpleName()); + } + } + return res; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java index 81ba4dd23..d40dbba42 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/jersey/provider/ProviderReporter.java @@ -20,10 +20,19 @@ public class ProviderReporter extends Reporter { private final Multimap prerender = HashMultimap.create(); + /** + * Create reporter. + */ public ProviderReporter() { super(JerseyProviderInstaller.class, "providers = "); } + /** + * @param provider provider type + * @param isHkManaged true for hk managed bean + * @param isLazy true for lazy bean + * @return reporter itself + */ public ProviderReporter provider(final Class provider, final boolean isHkManaged, final boolean isLazy) { // recognize all extension types and render lines accordingly for (Class ext : ProviderRenderUtil.detectProviderTypes(provider)) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/Plugin.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/Plugin.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/Plugin.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/Plugin.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java index 054ad825d..7b7614d72 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginInstaller.java @@ -12,6 +12,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; /** * Shortcut for guice multibindings mechanism. @@ -47,6 +49,12 @@ public void manualBinding(final Binder binder, final Class type, final Bi registerPlugin(binder, type); } + /** + * Plugin registration. + * + * @param binder binder + * @param type plugin type + */ @SuppressWarnings("unchecked") public void registerPlugin(final Binder binder, final Class type) { // multibindings registration (common for both registration types) @@ -82,4 +90,10 @@ private void registerNamedPlugin(final Binder binder, final Class plug public void report() { reporter.report(); } + + @Override + public List getRecognizableSigns() { + return Arrays.asList("@" + Plugin.class.getSimpleName() + " on class", + "custom annotation on class, annotated with " + "@" + Plugin.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java similarity index 82% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java index 733a76c6b..f5227ef35 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/plugin/PluginReporter.java @@ -24,16 +24,35 @@ public class PluginReporter extends Reporter { private final Multimap namedPlugins = HashMultimap.create(); private final Multimap plugins = HashMultimap.create(); + /** + * Create reporter. + */ public PluginReporter() { super(PluginInstaller.class, "plugins ="); } + /** + * Register named plugin. + * + * @param keyType plugin extension type + * @param extType plugin type + * @param key annotation key + * @param extension extension type + * @return reporter itself + */ public PluginReporter named(final Class keyType, final Class extType, final Object key, final Class extension) { namedPlugins.put(format(NAMED_KEY, keyType.getSimpleName(), extType.getSimpleName()), format(NAMED_LINE, key, RenderUtils.renderClassLine(extension))); return this; } + /** + * Register simple plugin. + * + * @param extType plugin type + * @param extension extension type + * @return reporter itself + */ public PluginReporter simple(final Class extType, final Class extension) { plugins.put(format(KEY, extType.getSimpleName()), format(LINE, RenderUtils.renderClassLine(extension))); return this; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java similarity index 81% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java index b93787479..83a79dddf 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/AdminContext.java @@ -3,8 +3,8 @@ import java.lang.annotation.*; /** - * Used together with {@link javax.servlet.annotation.WebServlet}, - * {@link javax.servlet.annotation.WebFilter} and {@link javax.servlet.annotation.WebListener} annotations + * Used together with {@link jakarta.servlet.annotation.WebServlet}, + * {@link jakarta.servlet.annotation.WebFilter} and {@link jakarta.servlet.annotation.WebListener} annotations * to specify target context. *

    * By default, web extensions target main context. Adding {@code @AdminContext} will mean registration diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java index 9c3ce89a1..89408169b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebFilterInstaller.java @@ -3,23 +3,26 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import io.dropwizard.jetty.setup.ServletEnvironment; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.feature.web.util.WebUtils; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller; import ru.vyarus.dropwizard.guice.module.installer.order.Order; import ru.vyarus.dropwizard.guice.module.installer.order.Ordered; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterRegistration; -import javax.servlet.annotation.WebFilter; -import javax.servlet.annotation.WebInitParam; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebInitParam; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; +import java.util.List; /** * Search for http filters annotated with {@link WebFilter} (servlet api annotation). Such filters will not @@ -45,7 +48,7 @@ */ @Order(100) public class WebFilterInstaller implements FeatureInstaller, - InstanceInstaller, Ordered { + InstanceInstaller, Ordered, WebInstaller { private final Reporter reporter = new Reporter(WebFilterInstaller.class, "filters ="); @@ -86,6 +89,7 @@ public void report() { reporter.report(); } + @SuppressWarnings("PMD.LooseCoupling") private void configure(final ServletEnvironment environment, final Filter filter, final String name, final WebFilter annotation) { final FilterRegistration.Dynamic mapping = environment.addFilter(name, filter); @@ -104,4 +108,10 @@ private void configure(final ServletEnvironment environment, final Filter filter } mapping.setAsyncSupported(annotation.asyncSupported()); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("implements " + Filter.class.getSimpleName() + + " + @" + WebFilter.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java index 5b5a2fe86..0294335f7 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/WebServletInstaller.java @@ -3,23 +3,26 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import io.dropwizard.jetty.setup.ServletEnvironment; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.feature.web.util.WebUtils; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller; import ru.vyarus.dropwizard.guice.module.installer.option.InstallerOptionsSupport; import ru.vyarus.dropwizard.guice.module.installer.order.Order; import ru.vyarus.dropwizard.guice.module.installer.order.Ordered; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import javax.servlet.ServletRegistration; -import javax.servlet.annotation.WebInitParam; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.annotation.WebInitParam; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import java.util.Collections; +import java.util.List; import java.util.Set; import static ru.vyarus.dropwizard.guice.module.installer.InstallersOptions.DenyServletRegistrationWithClash; @@ -52,7 +55,7 @@ */ @Order(90) public class WebServletInstaller extends InstallerOptionsSupport - implements FeatureInstaller, InstanceInstaller, Ordered { + implements FeatureInstaller, InstanceInstaller, Ordered, WebInstaller { private final Logger logger = LoggerFactory.getLogger(WebServletInstaller.class); private final Reporter reporter = new Reporter(WebServletInstaller.class, "servlets ="); @@ -111,4 +114,10 @@ private void configure(final ServletEnvironment environment, final HttpServlet s } mapping.setAsyncSupported(annotation.asyncSupported()); } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("extends " + HttpServlet.class.getSimpleName() + + " + @" + WebServlet.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java similarity index 80% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java index 6a8cc1567..69aad2430 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/ListenerReporter.java @@ -6,13 +6,14 @@ import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; -import javax.servlet.ServletContextAttributeListener; -import javax.servlet.ServletContextListener; -import javax.servlet.ServletRequestAttributeListener; -import javax.servlet.ServletRequestListener; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionIdListener; -import javax.servlet.http.HttpSessionListener; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; + import java.util.EventListener; import java.util.Map; @@ -37,10 +38,19 @@ public class ListenerReporter extends Reporter { private final Multimap prerender = HashMultimap.create(); + /** + * Create reporter. + */ public ListenerReporter() { super(WebListenerInstaller.class, "web listeners = "); } + /** + * Listener installed. + * + * @param type listener type + * @param contextMarkers context markers + */ @SuppressWarnings("unchecked") public void listener(final Class type, final String contextMarkers) { final String line = String.format(TAB + "%-2s %s", contextMarkers, RenderUtils.renderClassLine(type)); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java similarity index 88% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java index d73558db8..f9032a3a6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/SessionListenersSupport.java @@ -5,7 +5,7 @@ import com.google.common.collect.Multimap; import io.dropwizard.jetty.MutableServletContextHandler; import io.dropwizard.lifecycle.Managed; -import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.ee10.servlet.SessionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; @@ -27,10 +27,21 @@ public class SessionListenersSupport implements Managed { private final boolean failWithoutSession; private final Multimap listeners = LinkedListMultimap.create(); + /** + * Create support. + * + * @param failWithoutSession true to fail without session handler + */ public SessionListenersSupport(final boolean failWithoutSession) { this.failWithoutSession = failWithoutSession; } + /** + * Add listener for delayed registration. + * + * @param environment servlet environment + * @param listener listener + */ public void add(final MutableServletContextHandler environment, final EventListener listener) { listeners.put(environment, listener); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java index 458f40371..0c16265bd 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/listener/WebListenerInstaller.java @@ -2,24 +2,26 @@ import com.google.common.collect.ImmutableList; import io.dropwizard.jetty.MutableServletContextHandler; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext; import ru.vyarus.dropwizard.guice.module.installer.feature.web.util.WebUtils; import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller; import ru.vyarus.dropwizard.guice.module.installer.option.InstallerOptionsSupport; import ru.vyarus.dropwizard.guice.module.installer.order.Order; import ru.vyarus.dropwizard.guice.module.installer.order.Ordered; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; -import javax.servlet.ServletContextAttributeListener; -import javax.servlet.ServletContextListener; -import javax.servlet.ServletRequestAttributeListener; -import javax.servlet.ServletRequestListener; -import javax.servlet.annotation.WebListener; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionIdListener; -import javax.servlet.http.HttpSessionListener; +import jakarta.servlet.ServletContextAttributeListener; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletRequestAttributeListener; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.annotation.WebListener; +import jakarta.servlet.http.HttpSessionAttributeListener; +import jakarta.servlet.http.HttpSessionIdListener; +import jakarta.servlet.http.HttpSessionListener; +import java.util.Collections; import java.util.EventListener; import java.util.List; @@ -51,7 +53,7 @@ */ @Order(110) public class WebListenerInstaller extends InstallerOptionsSupport - implements FeatureInstaller, InstanceInstaller, Ordered { + implements FeatureInstaller, InstanceInstaller, Ordered, WebInstaller { private static final List> CONTEXT_LISTENERS = ImmutableList.of( ServletContextListener.class, @@ -128,4 +130,10 @@ private boolean hasMatch(final Class type, final List getRecognizableSigns() { + return Collections.singletonList("implements " + EventListener.class.getSimpleName() + + " + @" + WebListener.class.getSimpleName()); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java index 9f018839a..5c7ac630f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/feature/web/util/WebUtils.java @@ -3,10 +3,10 @@ import com.google.common.base.Strings; import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext; -import javax.servlet.Filter; -import javax.servlet.annotation.WebFilter; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; +import jakarta.servlet.Filter; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; /** * Web installers utilities. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java index c15dbfc27..4553bcb48 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/InstanceInstaller.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.installer.install; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java index 04dcff932..64da27bcf 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/JerseyInstaller.java @@ -25,7 +25,7 @@ * @since 16.11.2014 * @see ru.vyarus.dropwizard.guice.module.installer.feature.jersey.AbstractJerseyInstaller base class */ -public interface JerseyInstaller { +public interface JerseyInstaller extends WebInstaller { /** * Called on jersey start to inject extensions into HK context. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java index 3e9f03a64..497e80e63 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/TypeInstaller.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.installer.install; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; /** * Marker interface must be used together with {@code FeatureInstaller}. diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/WebInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/WebInstaller.java new file mode 100644 index 000000000..f12ceda77 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/WebInstaller.java @@ -0,0 +1,16 @@ +package ru.vyarus.dropwizard.guice.module.installer.install; + +/** + * Marker interface for installers, installing web objects (servlets, filters, rest resources). + * All installers of features available only with complete application start must be indicated as web installers + * (those extensions that are ignored when lightweight guicey test + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}) started). + *

    + * All {@link ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller} related installers are + * already marked as web. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public interface WebInstaller { +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java index 461ea11bf..885e61f39 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/BindingInstaller.java @@ -64,7 +64,7 @@ public interface BindingInstaller { * @see ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils#getDeclarationSource(Binding) * for errors reporting */ - default void manualBinding(Binder binder, Class type, Binding binding) { + default void manualBinding(final Binder binder, final Class type, final Binding binding) { // no actions by default } @@ -78,7 +78,7 @@ default void manualBinding(Binder binder, Class type, Binding binding) * rendering ({@link Stage#TOOL}) * @param type extension class */ - default void extensionBound(Stage stage, Class type) { + default void extensionBound(final Stage stage, final Class type) { // no actions by default } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/LazyBinding.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/LazyBinding.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/LazyBinding.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/install/binding/LazyBinding.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java index 6d5f1ec0a..0a007c970 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/AdminGuiceFilter.java @@ -2,7 +2,7 @@ import com.google.inject.servlet.GuiceFilter; -import javax.servlet.*; +import jakarta.servlet.*; import java.io.IOException; /** @@ -20,6 +20,11 @@ public class AdminGuiceFilter implements Filter { private final GuiceFilter filter; + /** + * Create admin filter for existing guice filter. + * + * @param filter guice filter + */ public AdminGuiceFilter(final GuiceFilter filter) { this.filter = filter; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java similarity index 70% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java index 1a87fdbab..ba63d9063 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/CommandSupport.java @@ -2,18 +2,27 @@ import com.google.common.base.Stopwatch; import com.google.inject.Injector; -import io.dropwizard.Application; -import io.dropwizard.cli.Command; -import io.dropwizard.cli.EnvironmentCommand; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.Application; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.cli.EnvironmentCommand; +import io.dropwizard.core.setup.Bootstrap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClassVisitor; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.CommandTime; @@ -31,8 +40,8 @@ private CommandSupport() { /** * Scans classpath to find commands and register them. - * Commands are instantiated using default constructor, but {@link io.dropwizard.cli.EnvironmentCommand} - * must have constructor with {@link io.dropwizard.Application} argument. + * Commands are instantiated using default constructor, but {@link io.dropwizard.core.cli.EnvironmentCommand} + * must have constructor with {@link io.dropwizard.core.Application} argument. * * @param bootstrap bootstrap object * @param scanner configured scanner instance @@ -41,8 +50,8 @@ private CommandSupport() { */ public static List registerCommands(final Bootstrap bootstrap, final ClasspathScanner scanner, final ConfigurationContext context) { - final Stopwatch timer = context.stat().timer(CommandTime); - final CommandClassVisitor visitor = new CommandClassVisitor(bootstrap); + final StatTimer timer = context.stat().timer(CommandTime); + final CommandClassVisitor visitor = new CommandClassVisitor(bootstrap, context.stat()); scanner.scan(visitor); context.registerCommands(visitor.getCommands()); timer.stop(); @@ -56,12 +65,16 @@ public static List registerCommands(final Bootstrap bootstrap, final Cl * * @param commands registered commands * @param injector guice injector object + * @param tracker stats tracker */ - public static void initCommands(final List commands, final Injector injector) { + public static void initCommands(final List commands, final Injector injector, + final StatsTracker tracker) { if (commands != null) { for (Command cmd : commands) { if (cmd instanceof EnvironmentCommand) { + final Stopwatch timer = tracker.detailTimer(DetailStat.Command, cmd.getClass()); injector.injectMembers(cmd); + timer.stop(); } } } @@ -69,17 +82,19 @@ public static void initCommands(final List commands, final Injector inj /** * Search catch all {@link Command} derived classes. - * Instantiate command with default constructor and {@link io.dropwizard.cli.EnvironmentCommand} - * using constructor with {@link io.dropwizard.Application} argument. + * Instantiate command with default constructor and {@link io.dropwizard.core.cli.EnvironmentCommand} + * using constructor with {@link io.dropwizard.core.Application} argument. */ private static class CommandClassVisitor implements ClassVisitor { private final Bootstrap bootstrap; + private final StatsTracker stats; // sort commands to unify order on different environments private final Set> commands = new TreeSet<>(Comparator.comparing(Class::getName)); private final List commandList = new ArrayList<>(); - CommandClassVisitor(final Bootstrap bootstrap) { + CommandClassVisitor(final Bootstrap bootstrap, final StatsTracker stats) { this.bootstrap = bootstrap; + this.stats = stats; } @Override @@ -88,12 +103,13 @@ public void visit(final Class type) { if (FeatureUtils.is(type, Command.class)) { try { final Command cmd; + final Stopwatch timer = stats.detailTimer(DetailStat.Command, type); if (EnvironmentCommand.class.isAssignableFrom(type)) { - cmd = (Command) type.getConstructor(Application.class) - .newInstance(bootstrap.getApplication()); + cmd = (Command) InstanceUtils.create(type, Application.class, bootstrap.getApplication()); } else { - cmd = (Command) type.newInstance(); + cmd = (Command) InstanceUtils.create(type); } + timer.stop(); commands.add((Class) type); commandList.add(cmd); bootstrap.addCommand(cmd); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java similarity index 67% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java index 03309125d..a39c54dfd 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsHolder.java @@ -8,6 +8,7 @@ import ru.vyarus.dropwizard.guice.module.installer.order.OrderComparator; import ru.vyarus.dropwizard.guice.module.installer.order.Ordered; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -22,15 +23,44 @@ */ public class ExtensionsHolder { private final List installers; + private final List scanExtensions = new ArrayList<>(); private List extensionsData; private final List> installerTypes; private final Map, List>> extensions = Maps.newHashMap(); + /** + * Create extensions holder. + * + * @param installers installers + */ public ExtensionsHolder(final List installers) { this.installers = installers; this.installerTypes = Lists.transform(installers, FeatureInstaller::getClass); } + /** + * @return extensions recognized by classpath scan + */ + public List getScanExtensions() { + return scanExtensions; + } + + /** + * Auto scan performed under configuration phase, but actual extensions registration only in run phase + * because manual extensions could be added at run phase (and manual extensions must be registered in priority). + * + * @param candidate potential extension + * @return true if extension accepted + */ + public boolean acceptScanCandidate(final Class candidate) { + final FeatureInstaller installer = ExtensionsSupport.findInstaller(candidate, installers); + final boolean recognized = installer != null; + if (recognized) { + scanExtensions.add(new ScanItem(candidate, installer)); + } + return recognized; + } + /** * Prepare known extensions for installation. * @@ -96,4 +126,37 @@ public void order() { } } } + + /** + * Extension item, detected with classpath scan. + */ + public static class ScanItem { + private final Class type; + private final FeatureInstaller installer; + + /** + * Create item. + * + * @param type extension type + * @param installer recognized installer + */ + public ScanItem(final Class type, final FeatureInstaller installer) { + this.type = type; + this.installer = installer; + } + + /** + * @return extension type + */ + public Class getType() { + return type; + } + + /** + * @return recognized installer + */ + public FeatureInstaller getInstaller() { + return installer; + } + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java index a38d893ce..aaabb0e8b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ExtensionsSupport.java @@ -48,7 +48,23 @@ private ExtensionsSupport() { public static boolean registerExtension(final ConfigurationContext context, final Class type, final boolean fromScan) { - final FeatureInstaller installer = findInstaller(type, context.getExtensionsHolder()); + final FeatureInstaller installer = findInstaller(type, context.getExtensionsHolder().getInstallers()); + return registerExtension(context, type, installer, fromScan); + } + + /** + * Register extension. + * + * @param context configuration context + * @param type extension type + * @param installer installer recognized extension (could be null) + * @param fromScan from classpath scan + * @return true if extension recognized + */ + public static boolean registerExtension(final ConfigurationContext context, + final Class type, + final FeatureInstaller installer, + final boolean fromScan) { boolean recognized = installer != null; // during classpath scan checks, non extension classes may come, so its not possible to move info creation // here from both branches @@ -58,6 +74,8 @@ public static boolean registerExtension(final ConfigurationContext context, info.setLazy(type.isAnnotationPresent(LazyBinding.class)); info.setJerseyManaged(JerseyBinding.isJerseyManaged(type, context.option(JerseyExtensionsManagedByGuice))); info.setInstaller(installer); + context.notifyExtensionRecognized(info); + } else if (!fromScan) { final ExtensionItemInfoImpl info = context.getOrRegisterExtension(type, fromScan); if (info.isOptional()) { @@ -89,7 +107,7 @@ public static boolean registerExtensionBinding(final ConfigurationContext contex // manually hidden annotation from scanning return false; } - final FeatureInstaller installer = findInstaller(type, context.getExtensionsHolder()); + final FeatureInstaller installer = findInstaller(type, context.getExtensionsHolder().getInstallers()); final boolean recognized = installer != null; if (recognized) { // important to force config creation for extension from scan to allow disabling by matcher @@ -108,6 +126,7 @@ public static boolean registerExtensionBinding(final ConfigurationContext contex info.setManualBinding(manualBinding); info.setInstaller(installer); + context.notifyExtensionRecognized(info); } return recognized; } @@ -158,13 +177,12 @@ public static void installExtensions(final ConfigurationContext context, final I * Search for matching installer. Extension may match multiple installer, but only one will be actually * used (note that installers are ordered). * - * @param type extension type - * @param holder extensions holder bean + * @param type extension type + * @param installers installers * @return matching installer or null if no matching installer found */ - @SuppressWarnings("unchecked") - private static FeatureInstaller findInstaller(final Class type, final ExtensionsHolder holder) { - for (FeatureInstaller installer : holder.getInstallers()) { + public static FeatureInstaller findInstaller(final Class type, final List installers) { + for (FeatureInstaller installer : installers) { if (installer.matches(type)) { return installer; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java new file mode 100644 index 000000000..0fc0a27c5 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java @@ -0,0 +1,620 @@ +package ru.vyarus.dropwizard.guice.module.installer.internal; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Binding; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.internal.MoreTypes; +import com.google.inject.internal.PrivateElementsImpl; +import com.google.inject.internal.Scoping; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; +import com.google.inject.spi.LinkedKeyBinding; +import com.google.inject.spi.PrivateElements; +import com.google.inject.spi.UntargettedBinding; +import com.google.inject.util.Modules; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.GuiceyOptions; +import ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule; +import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; +import ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils; +import ru.vyarus.dropwizard.guice.module.support.BootstrapAwareModule; +import ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule; +import ru.vyarus.dropwizard.guice.module.support.ConfigurationTreeAwareModule; +import ru.vyarus.dropwizard.guice.module.support.EnvironmentAwareModule; +import ru.vyarus.dropwizard.guice.module.support.OptionsAwareModule; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static ru.vyarus.dropwizard.guice.GuiceyOptions.AnalyzeGuiceModules; +import static ru.vyarus.dropwizard.guice.GuiceyOptions.InjectorStage; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersTime; +import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ModulesProcessingTime; + +/** + * Helper class for guice modules processing. + * + * @author Vyacheslav Rusakov + * @since 25.04.2018 + */ +@SuppressWarnings({"PMD.ExcessiveImports", "PMD.GodClass", "ClassFanOutComplexity", "PMD.CouplingBetweenObjects"}) +public final class ModulesSupport { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModulesSupport.class); + + private ModulesSupport() { + } + + /** + * Post-process registered modules by injecting bootstrap, configuration, environment and options objects. + * + * @param context configuration context + */ + @SuppressWarnings("unchecked") + public static void configureModules(final ConfigurationContext context) { + for (Module mod : context.getEnabledModules()) { + if (mod instanceof BootstrapAwareModule) { + ((BootstrapAwareModule) mod).setBootstrap(context.getBootstrap()); + } + if (mod instanceof ConfigurationAwareModule) { + ((ConfigurationAwareModule) mod).setConfiguration(context.getConfiguration()); + } + if (mod instanceof ConfigurationTreeAwareModule) { + ((ConfigurationTreeAwareModule) mod).setConfigurationTree(context.getConfigurationTree()); + } + if (mod instanceof EnvironmentAwareModule) { + ((EnvironmentAwareModule) mod).setEnvironment(context.getEnvironment()); + } + if (mod instanceof OptionsAwareModule) { + ((OptionsAwareModule) mod).setOptions(context.optionsReadOnly()); + } + } + } + + /** + * Prepares modules to use for injector creation (applies module overrides). + * + * @param context configuration context + * @return modules for injector creation + */ + public static Iterable prepareModules(final ConfigurationContext context) { + final StatTimer timer = context.stat().timer(ModulesProcessingTime); + final List overridingModules = context.getOverridingModules(); + // repackage normal modules to reveal all guice extensions + final List normalModules = analyzeModules(context, timer); + + final Iterable res = overridingModules.isEmpty() ? normalModules + : Collections.singletonList(Modules.override(normalModules).with(overridingModules)); + timer.stop(); + return res; + } + + /** + * Search for extensions in guice bindings (directly declared in modules). + * Only user provided modules are analyzed. Overriding modules are not analyzed. + *

    + * Use guice SPI. In order to avoid duplicate analysis in injector creation time, wrap + * parsed elements as new module (and use it instead of original modules). Also, if + * bound extension is disabled, target binding is simply removed (in order to + * provide the same disable semantic as with usual extensions). + * + * @param context configuration context + * @param modulesTimer modules processing timer + * @return list of repackaged modules to use + */ + private static List analyzeModules(final ConfigurationContext context, + final StatTimer modulesTimer) { + List modules = context.getNormalModules(); + final Boolean configureFromGuice = context.option(AnalyzeGuiceModules); + // one module mean no user modules registered + if (modules.size() > 1 && configureFromGuice) { + // analyzing only user bindings (excluding overrides and guicey technical bindings) + final GuiceBootstrapModule bootstrap = (GuiceBootstrapModule) modules.remove(modules.size() - 1); + try { + // find extensions and remove bindings if required (disabled extensions) + final StatTimer gtime = context.stat().timer(Stat.BindingsResolutionTime); + final List elements = new ArrayList<>( + Elements.getElements(context.option(InjectorStage), modules)); + gtime.stop(); + + // exclude analysis time from modules processing time (it's installer time) + modulesTimer.stop(); + analyzeAndFilterBindings(context, modules, elements); + modulesTimer.start(); + + // wrap raw elements into module to avoid duplicate work on guice startup and put back bootstrap + modules = Arrays.asList(Elements.getModule(elements), bootstrap); + } catch (Exception ex) { + // better show meaningful message then just fail entire startup with ambiguous message + // NOTE if guice configuration is not OK it will fail here too, but user will see injector creation + // error as last error in logs. + LOGGER.error("Failed to analyze guice bindings - skipping this step. Note that configuration" + + " from bindings may be switched off with " + GuiceyOptions.class.getSimpleName() + "." + + AnalyzeGuiceModules.name() + " option.", ex); + // recover and use original modules + modules.add(bootstrap); + if (!modulesTimer.isRunning()) { + modulesTimer.start(); + } + } + } + return modules; + } + + private static void analyzeAndFilterBindings(final ConfigurationContext context, + final List analyzedModules, + final List elements) { + final StatTimer itimer = context.stat().timer(InstallersTime); + final StatTimer timer = context.stat().timer(Stat.ExtensionsRecognitionTime); + final StatTimer analysisTimer = context.stat().timer(Stat.BindingsAnalysisTime); + final List disabledModules = prepareDisabledModules(context); + + final AnalysisResult result = analyzeElements(context, elements, disabledModules, null); + + if (!result.actuallyDisabledModules.isEmpty()) { + LOGGER.debug("Removed inner guice modules: {}", result.actuallyDisabledModules); + } + // must be triggered only once (at the end of overall analysis) + context.stat().count(Stat.RemovedInnerModules, result.actuallyDisabledModules.size()); + context.stat().count(Stat.RemovedBindingsCount, result.removedBindings.size()); + context.lifecycle().modulesAnalyzed(analyzedModules, result.extensions, + toModuleClasses(result.actuallyDisabledModules), result.removedBindings); + analysisTimer.stop(); + timer.stop(); + itimer.stop(); + } + + /** + * Actual guice modules analysis. Would be called for all guice modules and for each private module + * (because private module bindings are managed independently). + *

    + * Bindings for disabled extensions would be removed. + * + * @param context guicey context + * @param elements parsed guice module elements (all bindings) + * @param disabledModules disabled modules + * @param privateFilter custom extensions filter used for private modules analysis (only exposed bindings could be + * accepted) + * @return analysis report (with found extensions and removed bindings) + */ + private static AnalysisResult analyzeElements(final ConfigurationContext context, + final List elements, + final List disabledModules, + final Predicate privateFilter) { + context.stat().count(Stat.BindingsCount, elements.size()); + + final Set actuallyDisabledModules = new HashSet<>(); + final List removedBindings = new ArrayList<>(); + final List> extensions = new ArrayList<>(); + // extension may be recognized by linked key and linked keys may need to be removed too + // linked key -> binding + final Multimap linkedBindings = LinkedHashMultimap.create(); + + final Iterator it = elements.iterator(); + while (it.hasNext()) { + final Element element = it.next(); + if (isInDisabledModule(element, disabledModules, actuallyDisabledModules)) { + // remove all bindings under disabled modules + it.remove(); + context.stat().count(Stat.RemovedBindingsCount, 1); + continue; + } + // filter constants, listeners, aop etc. + if (element instanceof Binding + && detectExtensionAndRemoveBindingIfDisabled(context, (Binding) element, + extensions, linkedBindings, privateFilter)) { + it.remove(); + removedBindings.add((Binding) element); + } else if (element instanceof PrivateElements + // no need to analyze private modules inside private modules - no services could be exposed + && privateFilter == null && (boolean) context.option(GuiceyOptions.AnalyzePrivateGuiceModules)) { + // private module is a child injector with only some bindings exposed to the parent - processing it + // as a separate process (private moduel is an independent set of bindings) + final AnalysisResult result = analyzePrivateModule((PrivateElements) element, + context, disabledModules); + extensions.addAll(result.extensions); + removedBindings.addAll(result.removedBindings); + actuallyDisabledModules.addAll(result.actuallyDisabledModules); + // linkedBindings not added because they would be already processed for private module + } + } + // Recognize extensions in linked bindings and remove bindings for disabled extensions (entire chains) + // Could not be done earlier as entire chains must be analyzed + for (Binding binding : detectExtensionsInLinkedBindingsAndRemoveDisabled( + context, extensions, linkedBindings, privateFilter)) { + elements.remove(binding); + removedBindings.add(binding); + } + return new AnalysisResult(extensions, removedBindings, actuallyDisabledModules); + } + + /** + * Checks if provided binding key is a guicey extension. If detected extension is disabled, remove binding (note + * that related linked binding would be cleared later). + *

    + * If provided binding is linked binding - just register it. It can't be analyzed right ahead because linked + * bindings could for a chain and we should detect top-most class as an extension (without it, multiple classes + * would be registered for the same binding). + * + * @param context guicey context + * @param binding binding to analyze + * @param extensions list of already detected extensions + * @param linkedBindings already discovered linked bindings map + * @param privateFilter extension fileter for private module (because only exposed bindings could be analyzed) + * @return true if binding was removed (for disabled extension), false otherwise + */ + private static boolean detectExtensionAndRemoveBindingIfDisabled( + final ConfigurationContext context, + final Binding binding, + final List> extensions, + final Multimap linkedBindings, + final Predicate privateFilter) { + final Key key = binding.getKey(); + if (isPossibleExtension(key, privateFilter)) { + context.stat().count(Stat.AnalyzedBindingsCount, 1); + final Class type = key.getTypeLiteral().getRawType(); + if (context.isAcceptableAutoScanClass(type) + && ExtensionsSupport.registerExtensionBinding(context, type, + binding, BindingUtils.getTopDeclarationModule(binding))) { + LOGGER.debug("Extension detected from guice binding: {}", type.getSimpleName()); + extensions.add(type); + return !context.isExtensionEnabled(type); + } + } + // note if linked binding recognized as extension by its key - it would not be counted (not needed) + if (binding instanceof LinkedKeyBinding) { + // remember all linked bindings (do not recognize on first path to avoid linked binding check before + // real binding) + final LinkedKeyBinding linkedBind = (LinkedKeyBinding) binding; + linkedBindings.put(linkedBind.getLinkedKey(), linkedBind); + } + return false; + } + + private static boolean isPossibleExtension(final Key key, final Predicate filter) { + // extension bindings may be only unqualified + return key.getAnnotation() == null + // class only (no generified types) + && key.getTypeLiteral().getType() instanceof Class + // additional filter to check for other conditions (used for private modules) + && (filter == null || filter.test(key)); + } + + // links map is: linked type (end) -> binding + private static List detectExtensionsInLinkedBindingsAndRemoveDisabled( + final ConfigurationContext context, + final List> extensions, + final Multimap links, + final Predicate privateFilter) { + // try to recognize extensions in links + for (Map.Entry entry : links.entries()) { + final Key key = entry.getKey(); + final Class type = key.getTypeLiteral().getRawType(); + final LinkedKeyBinding binding = entry.getValue(); + if (!isPossibleExtension(key, privateFilter)) { + continue; + } + // try to detect extension in linked type (binding already analyzed so no need to count) + if (!extensions.contains(type) + && context.isAcceptableAutoScanClass(type) + && ExtensionsSupport.registerExtensionBinding(context, type, + binding, BindingUtils.getTopDeclarationModule(binding))) { + LOGGER.debug("Extension detected from guice linked binding: {}", type.getSimpleName()); + extensions.add(type); + } + } + // remove linked bindings for disabled extensions (entire chains) + final List removedExtensions = extensions.stream() + .filter(it -> !context.isExtensionEnabled(it)) + .map(Key::get) + .collect(Collectors.toList()); + return removeChains(removedExtensions, links); + } + + /** + * Pass in removed binding keys. Need to find all links ending on removed type and remove. + * Next, repeat with just removed types (to clean up entire chains because with it context may not start). + * For example: {@code bind(Interface1).to(Interface2); bind(Interface2).to(Extension)} + * Extension detected as extension, but if its disabled then link (Interface2 -> Extension) must be removed + * but without it Interface1 -> Interface2 remains and fail context because its just interfaces + * that's why entire chains must be removed. + * + * @param removed removed keys (to clean links leading to these keys) + * @param bindings all linked bindings (actually without links with recognized extension in left part) + * @return list of bindings to remove + */ + private static List removeChains(final List removed, + final Multimap bindings) { + final List newlyRemoved = new ArrayList<>(); + final List res = new ArrayList<>(); + + for (Key removedKey : removed) { + // remove all links ending on removed key + for (LinkedKeyBinding bnd : bindings.get(removedKey)) { + res.add(bnd); + newlyRemoved.add(bnd.getKey()); + } + } + + // continue removing chains + if (!newlyRemoved.isEmpty()) { + res.addAll(removeChains(newlyRemoved, bindings)); + } + return res; + } + + /** + * Search extensions in private module. In contrast to the usual module, in private module we could see only + * exposed bindings, and so we analyze just directly exposed bindings and linked chains from exposed bindings. + * For example: {@code expose(Ext)} - directly exposed extension, and {@code bind(Intrce).to(Ext); expose(Intrce)} + * - indirectly exposed extension, which still could be obtained through interface. + *

    + * Guicey extensions assumed to be resolved by class (you can get extension instance from guice context using + * extension class {@code injector.getInstance(Ext)}). In case of indirect exposure from private module extension + * would not be accessible by class. I did not want to complicate guicey introducing new "binding" key concept + * for extensions (assuming so many installers already rely on direct extension getting from injector). + * To work around this, guicey would ADD custom extension bindings (if required) and expose extension directly. + * This would not be visible in reports. + *

    + * Note that there would not be a "same class in two private modules" problem, because guicey counts only + * exposed extensions and so you will not be able to expose the same class twice into parent context. + *

    + * Disabling extensions will also lead to bindings remove in private module (this is possible because + * private module elements are MUTABLE (to some point) and so could be modified). In case of disabled module, + * all private module bindings would be removed. But it is also possible to remove sub module inside private + * module (but only first level!). + * + * @param privateModule private module + * @param context guicey context + * @param disabledModules disabled modules + * @return module analysis report (all findings must be added to the main analysis report) + */ + private static AnalysisResult analyzePrivateModule(final PrivateElements privateModule, + final ConfigurationContext context, + final List disabledModules) { + final PrivateElementsImpl module = (PrivateElementsImpl) privateModule; + + final PrivateExposedFilter filter = new PrivateExposedFilter(module); + + // IMPORTANT using mutable elements to be able to modify bindings (100% legal - gucie do it internally) + // After getElements() call, all further mutable collection changes would be ignored! + // Another moment: it would be possible to remove sub-modules (no matter that they are private) + final AnalysisResult result = analyzeElements(context, module.getElementsMutable(), disabledModules, filter); + + // missed bindings (not directly exposed extensions) + final List toExpose = new ArrayList<>(); + for (Class ext : result.extensions) { + // do not expose disabled extensions + if (context.isExtensionEnabled(ext)) { + final Key expose = Preconditions.checkNotNull(filter.detected.get(ext), + "Exposed key not found for detected extension : %s", ext.getName()); + if (!expose.getTypeLiteral().getRawType().equals(ext)) { + LOGGER.debug( + "Extension {} is indirectly exposed from private module by key {}. Adding direct expose", + ext, expose); + toExpose.add(ext); + } + } + } + exposeIndirectPrivateModuleExtensions(toExpose, filter.boundKeys, result.removedBindings, module); + + return result; + } + + // registering additional expose keys, so extensions become available by class directly (required) + // Implementation hack: have to override existing immutable map with reflection + // (note that new exposures would not be visible in the report because it analyzes modules!) + + /** + * + * bind(Iface.class).to(Ext.class) + * expose(IFace.class) + * + * For extensions detected in the exposed chain, but not directly exposed, direct expose is required. + * But expose requires existing binding: in the example above {@code expose(Ext.class)} would not work + * as guice requires explicit binding (at least, untargetted {@code bind(Ext.class)}). So guice would not only + * add expose for extension, but will register untargetted binding if there is no existing binding for extension. + *

    + * Also, generic analysis logic removes only bindings for disabled extensions, but an extension key will remain + * as exposed - it must be also removed. + * + * @param notExposed detected private module extensions without expose + * @param boundKeys all binging keys, used in module + * @param removedBindings removed bindings (required to remove exposed keys) + * @param module private module element (to modify) + */ + @SuppressWarnings("unchecked") + @SuppressFBWarnings("REC_CATCH_EXCEPTION") + private static void exposeIndirectPrivateModuleExtensions(final List notExposed, + final Set> boundKeys, + final List removedBindings, + final PrivateElementsImpl module) { + // need to add expose and untargetted bindings to be able to access extension by extension class directly + try { + + // exposed bindings field (can't access it in a "normal way" at this stage) + final Field exposedKeysField = PrivateElementsImpl.class.getDeclaredField("exposedKeysToSources"); + exposedKeysField.setAccessible(true); + final Map, Object> exposedKeys = new HashMap<>((Map, Object>) exposedKeysField.get(module)); + // existing exposes for removed bindings must be cleared + for (Binding binding : removedBindings) { + exposedKeys.remove(binding.getKey()); + if (binding instanceof LinkedKeyBinding) { + exposedKeys.remove(((LinkedKeyBinding) binding).getLinkedKey()); + } + } + + // package private - no way to use directly + final Constructor ctor = Class.forName("com.google.inject.internal.UntargettedBindingImpl") + .getDeclaredConstructor(Object.class, Key.class, Scoping.class); + ctor.setAccessible(true); + + // add missed keys + for (Class ext : notExposed) { + LOGGER.debug("Registering synthetic bindings for private module to expose extension: {}", + ext.getName()); + final Key key = MoreTypes.canonicalizeKey(Key.get(ext)); + if (!boundKeys.contains(key)) { + // add required untargetted binding for added expose + module.getElementsMutable().add((UntargettedBinding) ctor.newInstance( + "Synthetic binding to expose extension directly: " + ext.getName(), key, Scoping.UNSCOPED)); + } + exposedKeys.put(key, "Synthetic exposure for extension: " + ext.getName()); + } + + exposedKeysField.set(module, ImmutableMap.copyOf(exposedKeys)); + exposedKeysField.setAccessible(false); + ctor.setAccessible(false); + } catch (Exception e) { + throw new IllegalStateException("Failed to override exposed keys for private module", e); + } + } + + /** + * Find (possible) exposed key in the linked bindings chain. In the simplest case - key itself. + * + * @param key key to check + * @param linkedBindings all linked bindings in private module + * @param exposedKeys all exposed keys in private module + * @return exposed key (could be provided key itself) or null if key unreachable outside private module + */ + private static Key findExposed(final Key key, + final Multimap linkedBindings, + final Set> exposedKeys) { + Key res = exposedKeys.contains(key) ? key : null; + if (res == null) { + for (LinkedKeyBinding target : linkedBindings.get(key)) { + final Key exposed = findExposed(target.getKey(), linkedBindings, exposedKeys); + if (exposed != null) { + res = exposed; + break; + } + } + } + return res; + } + + private static List prepareDisabledModules(final ConfigurationContext context) { + final List res = new ArrayList<>(); + for (Class cls : context.getDisabledModuleTypes()) { + res.add(cls.getName()); + } + return res; + } + + private static boolean isInDisabledModule(final Element element, + final List disabled, + final Set actuallyDisabled) { + if (!disabled.isEmpty()) { + final List modules = BindingUtils.getModules(element); + // need to check from top modules to lower, otherwise removed modules list will be incorrect + for (int i = modules.size() - 1; i >= 0; i--) { + final String mod = modules.get(i); + if (disabled.contains(mod)) { + actuallyDisabled.add(mod); + return true; + } + } + } + return false; + } + + private static List> toModuleClasses(final Set modules) { + if (modules.isEmpty()) { + return Collections.emptyList(); + } + final List> res = new ArrayList<>(); + for (String mod : modules) { + res.add(BindingUtils.getModuleClass(mod)); + } + return res; + } + + /** + * Guice modules analysis result. + */ + @SuppressWarnings("VisibilityModifier") + private static class AnalysisResult { + public final List> extensions; + public final List removedBindings; + public final Set actuallyDisabledModules; + + AnalysisResult(final List> extensions, + final List removedBindings, + final Set actuallyDisabledModules) { + this.extensions = extensions; + this.removedBindings = removedBindings; + this.actuallyDisabledModules = actuallyDisabledModules; + } + } + + /** + * Extensions filter for private modules: only exposed extensions, or extensions in the exposed chain could + * be accepted. + *

    + * For example: + * + * bind(Iface.class).to(Ext.class) + * expose(Iface.class) + * + * Here only Iface is exposed, but it provides access for actual Ext class and so extension should be considered + * as acceptable. + */ + @SuppressWarnings("VisibilityModifier") + private static class PrivateExposedFilter implements Predicate { + + public final Set> boundKeys; + // collect exposure keys for extension (important for not directly exposed extensions detection) + public final Map detected = new HashMap<>(); + + // all linked bindings in private module so we could track the entire chains + // linked key - binding (to easily go in backwards direction) + private final Multimap linkedBindings; + // all exposed keys + private final Set> exposedKeys; + + PrivateExposedFilter(final PrivateElementsImpl module) { + exposedKeys = module.getExposedKeys(); + boundKeys = new HashSet<>(); + linkedBindings = LinkedHashMultimap.create(); + // have to collect links before analysis - otherwise exposure test filter couldn't work correctly + for (final Element element : module.getElementsMutable()) { + if (element instanceof Binding) { + boundKeys.add(((Binding) element).getKey()); + } + if (element instanceof LinkedKeyBinding) { + final LinkedKeyBinding linkedBind = (LinkedKeyBinding) element; + linkedBindings.put(linkedBind.getLinkedKey(), linkedBind); + } + } + } + + @Override + public boolean test(final Key key) { + final Key exposed = findExposed(key, linkedBindings, exposedKeys); + if (exposed != null) { + detected.put(key.getTypeLiteral().getRawType(), exposed); + } + return exposed != null; + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java index 42b7a1a81..a5e4f98ed 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/InstallerOptionsSupport.java @@ -28,7 +28,7 @@ public void setOptions(final Options options) { * @see Options#get(java.lang.Enum) for details * @see ru.vyarus.dropwizard.guice.GuiceyOptions for options example */ - protected V option(final T option) { + protected & Option> V option(final T option) { return options.get(option); } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/WithOptions.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/WithOptions.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/WithOptions.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/option/WithOptions.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Order.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Order.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Order.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Order.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/OrderComparator.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/OrderComparator.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/OrderComparator.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/OrderComparator.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Ordered.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Ordered.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Ordered.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/order/Ordered.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java index f6fc878a8..b10cef305 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClassVisitor.java @@ -6,6 +6,7 @@ * @author Vyacheslav Rusakov * @since 01.09.2014 */ +@FunctionalInterface public interface ClassVisitor { /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java similarity index 78% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java index 87c8550ff..8565d269b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/ClasspathScanner.java @@ -2,15 +2,15 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; -import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.scanner.util.OReflectionHelper; import java.lang.reflect.Modifier; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -28,6 +28,7 @@ * @author Vyacheslav Rusakov * @since 31.08.2014 */ +@SuppressFBWarnings("CT_CONSTRUCTOR_THROW") public class ClasspathScanner { private static final int SCAN_THRESHOLD = 1000; @@ -36,15 +37,31 @@ public class ClasspathScanner { private final StatsTracker tracker; private final Set packages; + private final boolean acceptProtectedClasses; private List scanned; + /** + * Create a scanner. + * + * @param packages packages to scan + */ public ClasspathScanner(final Set packages) { // for backwards compatibility allow using without tracker - this(packages, null); + this(packages, false, null); } - public ClasspathScanner(final Set packages, final StatsTracker tracker) { + /** + * Create a scanner. + * + * @param packages packages to scan + * @param acceptProtectedClasses look protected classes + * @param tracker tracker instance + */ + public ClasspathScanner(final Set packages, + final boolean acceptProtectedClasses, + final StatsTracker tracker) { this.packages = validate(packages); + this.acceptProtectedClasses = acceptProtectedClasses; this.tracker = tracker; // perform scan before to fill cache and get accurate traversing stats performScan(); @@ -78,12 +95,7 @@ public void cleanup() { */ private Set validate(final Set packages) { final List pkg = Lists.newArrayList(packages); - Collections.sort(pkg, new Comparator() { - @Override - public int compare(final String o1, final String o2) { - return Integer.compare(o1.length(), o2.length()); - } - }); + pkg.sort(Comparator.comparingInt(String::length)); for (int i = 0; i < pkg.size(); i++) { final String path = pkg.get(i); for (int j = i + 1; j < pkg.size(); j++) { @@ -97,15 +109,15 @@ public int compare(final String o1, final String o2) { return packages; } - @SuppressWarnings("PMD.PrematureDeclaration") private void performScan() { - final Stopwatch timer = tracker == null ? null : tracker.timer(ScanTime); + final StatTimer timer = tracker == null ? null : tracker.timer(ScanTime); int count = 0; scanned = Lists.newArrayList(); for (String pkg : packages) { final List> found; try { - found = OReflectionHelper.getClassesFor(pkg, Thread.currentThread().getContextClassLoader()); + found = OReflectionHelper.getClassesFor( + pkg, Thread.currentThread().getContextClassLoader(), acceptProtectedClasses); } catch (ClassNotFoundException e) { throw new IllegalStateException("Failed to scan classpath", e); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/InvisibleForScanner.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/InvisibleForScanner.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/InvisibleForScanner.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/InvisibleForScanner.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java similarity index 77% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java index 9f61f4ff0..b730ed706 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/scanner/util/OReflectionHelper.java @@ -1,5 +1,7 @@ package ru.vyarus.dropwizard.guice.module.installer.scanner.util; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -20,15 +22,39 @@ * * @author Antony Stubbs */ -@SuppressWarnings({"PMD", "all"}) +@SuppressWarnings("all") public final class OReflectionHelper { private static final String CLASS_EXTENSION = ".class"; private OReflectionHelper() { } + /** + * Preserved for backwards compatibility. + * + * @param iPackageName package + * @param iClassLoader class loader + * @return classes in package + * @throws ClassNotFoundException on error + */ public static List> getClassesFor(final String iPackageName, final ClassLoader iClassLoader) throws ClassNotFoundException { + return getClassesFor(iPackageName, iClassLoader, false); + } + + /** + * Search for classes in package. + * + * @param iPackageName package + * @param iClassLoader class loader + * @param acceptProtectedClasses true to accept protected classes + * @return classes in package + * @throws ClassNotFoundException on error + */ + @SuppressFBWarnings("DCN_NULLPOINTER_EXCEPTION") + public static List> getClassesFor(final String iPackageName, + final ClassLoader iClassLoader, + final boolean acceptProtectedClasses) throws ClassNotFoundException { // This will hold a list of directories matching the pckgname. // There may be more than one if a package is split over multiple jars/paths final List> classes = new ArrayList>(); @@ -53,7 +79,7 @@ public static List> getClassesFor(final String iPackageName, if (e.getName().startsWith(iPackageName.replace('.', '/')) && e.getName().endsWith(CLASS_EXTENSION)) { final String className = e.getName().replace("/", ".").substring(0, e.getName().length() - 6); final Class cls = Class.forName(className, true, iClassLoader); - if (isAcceptibleClass(cls)) { + if (isAcceptibleClass(cls, acceptProtectedClasses)) { classes.add(cls); } } @@ -79,13 +105,13 @@ public static List> getClassesFor(final String iPackageName, if (files != null) { for (File file : files) { if (file.isDirectory()) { - classes.addAll(findClasses(file, iPackageName, iClassLoader)); + classes.addAll(findClasses(file, iPackageName, iClassLoader, acceptProtectedClasses)); } else { String className; if (file.getName().endsWith(CLASS_EXTENSION)) { className = file.getName().substring(0, file.getName().length() - CLASS_EXTENSION.length()); final Class cls = Class.forName(iPackageName + '.' + className, true, iClassLoader); - if (isAcceptibleClass(cls)) { + if (isAcceptibleClass(cls, acceptProtectedClasses)) { classes.add(cls); } } @@ -108,7 +134,8 @@ public static List> getClassesFor(final String iPackageName, * @throws ClassNotFoundException */ private static List> findClasses(final File iDirectory, String iPackageName, - ClassLoader iClassLoader) throws ClassNotFoundException { + ClassLoader iClassLoader, + boolean acceptProtected) throws ClassNotFoundException { final List> classes = new ArrayList>(); if (!iDirectory.exists()) { return classes; @@ -124,11 +151,11 @@ private static List> findClasses(final File iDirectory, String iPackage if (file.getName().contains(".")) { continue; } - classes.addAll(findClasses(file, iPackageName, iClassLoader)); + classes.addAll(findClasses(file, iPackageName, iClassLoader, acceptProtected)); } else if (file.getName().endsWith(CLASS_EXTENSION)) { className = file.getName().substring(0, file.getName().length() - CLASS_EXTENSION.length()); final Class cls = Class.forName(iPackageName + '.' + className, true, iClassLoader); - if (isAcceptibleClass(cls)) { + if (isAcceptibleClass(cls, acceptProtected)) { classes.add(cls); } } @@ -137,8 +164,10 @@ private static List> findClasses(final File iDirectory, String iPackage return classes; } - private static boolean isAcceptibleClass(final Class type) { + private static boolean isAcceptibleClass(final Class type, final boolean acceptProtected) { // only public non-anonymous classes allowed - return Modifier.isPublic(type.getModifiers()); + return Modifier.isPublic(type.getModifiers()) || (acceptProtected && + // package private or protected + (type.getModifiers() == 0 || Modifier.isProtected(type.getModifiers()))); } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java index d6ad152d7..a7df572b9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BindingUtils.java @@ -2,10 +2,13 @@ import com.google.inject.Binding; import com.google.inject.Module; +import com.google.inject.ScopeAnnotation; import com.google.inject.internal.util.StackTraceElements; import com.google.inject.spi.Element; import com.google.inject.spi.ElementSource; +import jakarta.inject.Scope; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; @@ -149,4 +152,29 @@ private static List replaceLambdaModuleClasses(final List module } return res; } + + /** + * Searches for scoping annotation on class. Base classes are not checked as scope is not inheritable. + * + * @param type class to search for + * @param countGuiceSpecific true to count guice-specific annotations (with {@link ScopeAnnotation}) + * @return detected annotation or null + */ + public static Class findScopingAnnotation(final Class type, + final boolean countGuiceSpecific) { + Class res = null; + for (Annotation ann : type.getAnnotations()) { + final Class annType = ann.annotationType(); + if (annType.isAnnotationPresent(Scope.class)) { + res = annType; + break; + } + // guice has special marker annotation + if (countGuiceSpecific && annType.isAnnotationPresent(ScopeAnnotation.class)) { + res = annType; + break; + } + } + return res; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java similarity index 65% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java index 381987e13..5a9d77257 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/BundleSupport.java @@ -1,13 +1,16 @@ package ru.vyarus.dropwizard.guice.module.installer.util; import com.google.common.base.MoreObjects; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; import com.google.common.collect.Lists; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.setup.Bootstrap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.dropwizard.guice.module.context.ConfigItem; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.context.info.ItemId; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; @@ -20,7 +23,7 @@ import java.util.stream.Collectors; /** - * Utility class to work with registered {@link io.dropwizard.ConfiguredBundle} objects within dropwizard + * Utility class to work with registered {@link io.dropwizard.core.ConfiguredBundle} objects within dropwizard * {@link Bootstrap} object. * * @author Vyacheslav Rusakov @@ -42,33 +45,59 @@ private BundleSupport() { * * Bundles duplicates are checked by type: only one bundle instance may be registered. * - * @param context bundles context + * @param context configuration context */ public static void initBundles(final ConfigurationContext context) { + final List> path = new ArrayList<>(); final List bundles = context.getEnabledBundles(); - final GuiceyBootstrap guiceyBootstrap = new GuiceyBootstrap(context, bundles); + final List initOrder = new ArrayList<>(); + final GuiceyBootstrap guiceyBootstrap = new GuiceyBootstrap(context, path, initOrder); + + initBundles(context, guiceyBootstrap, path, initOrder, bundles); + context.storeBundlesInitOrder(initOrder); + context.lifecycle().bundlesInitialized(new ArrayList<>(initOrder), context.getDisabledBundles(), + context.getIgnoredItems(ConfigItem.Bundle)); + } - for (GuiceyBundle bundle : new ArrayList<>(bundles)) { + /** + * Point of root (registered in guice bundle) bundles installation. Also, called in guicey bootstrap + * for transitive bundles installation (immediate initialization). + * + * @param context configuration context + * @param bootstrap guicey bootstrap object + * @param path transitive bundles installation path + * @param initOrder bundles initialization order (appendable) + * @param bundles bundles to install + */ + public static void initBundles(final ConfigurationContext context, + final GuiceyBootstrap bootstrap, + final List> path, + final List initOrder, + final List bundles) { + for (GuiceyBundle bundle : bundles) { // iterating bundles as tree in order to detect cycles - initBundle(Collections.emptyList(), bundle, bundles, context, guiceyBootstrap); + initBundle(path, initOrder, bundle, context, bootstrap); } - - context.lifecycle().bundlesInitialized(context.getEnabledBundles(), context.getDisabledBundles(), - context.getIgnoredItems(ConfigItem.Bundle)); } /** - * Run all enabled bundles. + * Run all enabled bundles (and delayed configurations). * * @param context bundles context * @throws Exception if something goes wrong */ public static void runBundles(final ConfigurationContext context) throws Exception { final GuiceyEnvironment env = new GuiceyEnvironment(context); - for (GuiceyBundle bundle : context.getEnabledBundles()) { + // process delayed configurations before bundles + context.processDelayedConfigurations(env); + // important to process bundles in the same order as they were initialized + final List bundlesOrdered = context.getBundlesOrdered(); + for (GuiceyBundle bundle : bundlesOrdered) { + final Stopwatch timer = context.stat().detailTimer(DetailStat.BundleRun, bundle.getClass()); bundle.run(env); + timer.stop(); } - context.lifecycle().bundlesStarted(context.getEnabledBundles()); + context.lifecycle().bundlesStarted(bundlesOrdered); } /** @@ -125,11 +154,10 @@ public static List findBundles(final Bootstrap bootstrap, final Class } private static void initBundle(final List> path, + final List initOrder, final GuiceyBundle bundle, - final List wrk, final ConfigurationContext context, final GuiceyBootstrap bootstrap) { - wrk.clear(); final Class bundleType = bundle.getClass(); if (path.contains(bundleType)) { @@ -138,24 +166,28 @@ private static void initBundle(final List> path, path.stream().map(Class::getSimpleName).collect(Collectors.joining(" -> ")) .replace(name, "( " + name), name)); } - LOGGER.debug("Initializing bundle ({} level): {}", path.size() + 1, bundleType.getName()); + // same path instance used for all bundles installation, so it's important to clear its state + path.add(bundleType); + LOGGER.debug("Initializing bundle ({} level): {}", path.size(), bundleType.getName()); // disabled bundles are not processed (so nothing will be registered from it) // important to check here because transitive bundles may appear to be disabled final ItemId id = ItemId.from(bundle); if (context.isBundleEnabled(id)) { - context.openScope(id); - bundle.initialize(bootstrap); - context.closeScope(); - } - - if (!wrk.isEmpty()) { - final List> nextPath = new ArrayList<>(path); - nextPath.add(bundleType); - for (GuiceyBundle nextBundle : new ArrayList<>(wrk)) { - initBundle(nextPath, nextBundle, wrk, context, bootstrap); + final ItemId currentScope = context.replaceContextScope(id); + final Stopwatch timer = context.stat().detailTimer(DetailStat.BundleInit, bundleType); + try { + bundle.initialize(bootstrap); + } catch (Exception ex) { + Throwables.throwIfUnchecked(ex); + throw new IllegalStateException("Guicey bundle initialization failed", ex); } + timer.stop(); + // collect bundles in initialization order to run at the same order + initOrder.add(bundle); + context.replaceContextScope(currentScope); } + path.remove(bundleType); } @SuppressWarnings("unchecked") diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java similarity index 99% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java index 877c8b35d..616f9ddf7 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/FeatureUtils.java @@ -129,7 +129,7 @@ public static Method findMethod(final Class type, final String name, final Cl */ @SuppressWarnings("unchecked") public static T invokeMethod(final Method method, final Object instance, final Object... args) { - final boolean acc = method.isAccessible(); + final boolean acc = method.canAccess(instance); method.setAccessible(true); try { return (T) method.invoke(instance, args); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/InstanceUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/InstanceUtils.java new file mode 100644 index 000000000..0e9b9a83f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/InstanceUtils.java @@ -0,0 +1,114 @@ +package ru.vyarus.dropwizard.guice.module.installer.util; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Class instance creation utility (to gather all instantiations in one place). + * + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +public final class InstanceUtils { + + private InstanceUtils() { + } + + /** + * Create a new instance using no-args constructor. + * + * @param type class + * @param instance type + * @return class instance + */ + public static T create(final Class type) { + try { + // getDeclaredConstructor because getConstructors does not returns default constructor + return type.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate class with default constructor: " + + type.getName(), ex); + } + } + + /** + * Shortcut for {@link #create(Class, Class[], Object...)} for one constructor argument. + * + * @param type class + * @param param constructor parameter type + * @param arg parameter value + * @param instance type + * @return object instance + */ + public static T create(final Class type, final Class param, final Object arg) { + return create(type, new Class[]{param}, arg); + } + + /** + * Create a new instance using constructor with provided parameters and values. + * + * @param type class + * @param param constructor parameter types + * @param args constructor arguments + * @param instance type + * @return object instance + */ + public static T create(final Class type, final Class[] param, final Object... args) { + try { + // only public constructors + return type.getConstructor(param).newInstance(args); + } catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate class: " + type.getName() + + "\n\t with constructor params: " + + Arrays.stream(param).map(Class::getSimpleName).collect(Collectors.joining(", ")) + + "\n\t and values: " + + Arrays.stream(args).map(String::valueOf).collect(Collectors.joining(", ")), ex); + } + } + + /** + * Create new instance using constructor with provided params and nulls as values. + * + * @param type class + * @param param constructor params + * @param instance type + * @return object instance + */ + public static T createWithNulls(final Class type, final Class... param) { + final Object[] args = new Object[param.length]; + Arrays.fill(args, null); + return create(type, param, args); + } + + /** + * For tests ONLY! + *

    + * Tries to find constructor with the smallest amount of arguments and use it with nulls to instantiate object. + *

    + * WARNING: primitive arguments are not supported! + * + * @param type class + * @param target type + * @return class instance + */ + @SuppressWarnings("PMD.PreserveStackTrace") + public static T createWithAnyConstructor(final Class type) { + try { + return create(type); + } catch (RuntimeException ex) { + // fallback to any constructor with null arguments + Constructor cand = null; + for (Constructor ctor : type.getDeclaredConstructors()) { + if (cand == null || cand.getParameterCount() > ctor.getParameterCount()) { + cand = ctor; + } + } + if (cand != null) { + return createWithNulls(type, cand.getParameterTypes()); + } else { + throw new IllegalStateException("Failed to find suitable constructor for " + type); + } + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java similarity index 86% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java index 4dcdab886..9851cd76e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/JerseyBinding.java @@ -16,9 +16,9 @@ import ru.vyarus.java.generics.resolver.context.GenericsContext; import ru.vyarus.java.generics.resolver.context.container.ParameterizedTypeImpl; -import javax.annotation.Priority; -import javax.inject.Provider; -import javax.inject.Singleton; +import jakarta.annotation.Priority; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; import java.lang.reflect.Type; import java.util.List; import java.util.function.Supplier; @@ -99,7 +99,7 @@ public static void bindComponent(final AbstractBinder binder, final Injector inj /** * Binds jersey {@link Supplier}. If bean is {@link JerseyManaged} then registered directly as - * factory. Otherwise register factory through special "lazy bridge" to delay guice factory bean instantiation. + * factory. Otherwise, register factory through special "lazy bridge" to delay guice factory bean instantiation. * Also registers factory directly (through wrapper to be able to inject factory by its type). *

    * NOTE: since jersey 2.26 jersey don't use hk2 directly and so all HK interfaces replaced by java 8 interfaces. @@ -138,21 +138,26 @@ public static void bindFactory(final AbstractBinder binder, final Injector i *

    If type is {@link JerseyManaged}, binds directly. * Otherwise, use guice "bridge" factory to lazily bind type.

    * - * @param binder jersey binder - * @param injector guice injector - * @param type type which implements specific jersey interface or extends class - * @param specificType specific jersey type (interface or abstract class) - * @param jerseyManaged true if bean must be managed by jersey, false to bind guice managed instance - * @param singleton true to force singleton scope - * @param autoQualify mimic default jersey behaviour by qualifying user providers with @Custom + * @param binder jersey binder + * @param injector guice injector + * @param type type which implements specific jersey interface or extends class + * @param specificType specific jersey type (interface or abstract class) + * @param jerseyManaged true if bean must be managed by jersey, false to bind guice managed instance + * @param singleton true to force singleton scope + * @param autoQualify mimic default jersey behaviour by qualifying user providers with @Custom + * @param instanceGuiceBinding true to bind by instance instead of factory "wrapper" (required for + * {@link org.glassfish.jersey.server.model.ModelProcessor} extensions due to + * initialization specifics) */ + @SuppressWarnings("checkstyle:ParameterNumber") public static void bindSpecificComponent(final AbstractBinder binder, final Injector injector, final Class type, final Class specificType, final boolean jerseyManaged, final boolean singleton, - final boolean autoQualify) { + final boolean autoQualify, + final boolean instanceGuiceBinding) { // resolve generics of specific type final GenericsContext context = GenericsResolver.resolve(type).type(specificType); final List genericTypes = context.genericTypes(); @@ -167,7 +172,9 @@ public static void bindSpecificComponent(final AbstractBinder binder, optionalSingleton( // @Priority mirroring is very important for providers prioritize( - binder.bindFactory(new GuiceComponentFactory<>(injector, type)).to(type).to(bindingType), + (instanceGuiceBinding ? binder.bind(injector.getInstance(type)) + : binder.bindFactory(new GuiceComponentFactory<>(injector, type))) + .to(type).to(bindingType), autoQualify, type), singleton); } @@ -175,7 +182,7 @@ public static void bindSpecificComponent(final AbstractBinder binder, /** * Used to bind jersey beans in guice context (lazily). Guice context is started first, so there is - * no way to bind instances. Instead "lazy bridge" installed, which will resolve target type on first call. + * no way to bind instances. Instead, "lazy bridge" installed, which will resolve target type on first call. * Guice is not completely started and direct injector lookup is impossible here, so lazy injector provider used. * * @param binder guice binder diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java index 3154a1492..ba5ef58bb 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PathUtils.java @@ -15,6 +15,9 @@ */ public final class PathUtils { + /** + * Slash. + */ public static final String SLASH = "/"; private static final Pattern PATH_DIRTY_SLASHES = Pattern.compile("(? + * For example, "some/path" normalized to "/some/path" and "http://localhost/some/path" preserved as is + * "http://localhost/some/path". + *

    + * Method mainly assumed to be used to format rest paths. With the slash at the beginning and no slash at the end + * makes them easy to concatenate. + * + * @param path path to normalize + * @return normalized path + */ + public static String normalizeAbsolutePath(final String path) { + String res = trimSlashes(normalize(path)); + if (!res.toLowerCase().startsWith("http")) { + res = "/" + res; + } + return res; + } + /** * Normalization for classpath resource path. Rules: *

      diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java index 78fb7c59b..0a5766e6a 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/PropertyUtils.java @@ -71,10 +71,10 @@ private static List> toClasses(final Iterable list, final M return res; } - private static List toInstances(final Iterable> list) throws Exception { + private static List toInstances(final Iterable> list) { final List res = Lists.newArrayList(); for (Class cls : list) { - res.add(cls.newInstance()); + res.add(InstanceUtils.create(cls)); } return res; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java index 67b12e9d6..19d93ff07 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/Reporter.java @@ -14,7 +14,13 @@ */ @SuppressWarnings("PMD.AvoidStringBufferField") public class Reporter { + /** + * Newline. + */ public static final String NEWLINE = String.format("%n"); + /** + * Tab. + */ public static final String TAB = " "; // marker to be able switch off reports easily @@ -25,6 +31,12 @@ public class Reporter { private int counter; private boolean wasEmptyLine; + /** + * Create reporter. + * + * @param type installer type + * @param title title + */ public Reporter(final Class type, final String title) { this.logger = LoggerFactory.getLogger(type); this.message = new StringBuilder(); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/StackUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/StackUtils.java new file mode 100644 index 000000000..58347edd7 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/installer/util/StackUtils.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.module.installer.util; + +import com.google.common.collect.ImmutableList; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; + +import java.util.List; +import java.util.Optional; + +/** + * Utility for obtaining source file reference on method call. Used internally to track test extensions registration + * (to show navigation links in the debug report). + * + * @author Vyacheslav Rusakov + * @since 06.03.2025 + */ +public final class StackUtils { + + private static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + private static final List> SHARED_STATE_INFRA = ImmutableList.of( + SharedConfigurationState.class, + GuiceyBootstrap.class, + GuiceyEnvironment.class, + DropwizardAwareModule.class, + Optional.class + ); + + private StackUtils() { + } + + /** + * @param skip classes to skip in stack + * @return caller stack frame + */ + @SuppressWarnings("checkstyle:BooleanExpressionComplexity") + public static Optional getCaller(final List> skip) { + return WALKER.walk(stream -> + stream.dropWhile(frame -> { + final Class type = frame.getDeclaringClass(); + return type.equals(StackUtils.class) + || type.getPackageName().startsWith("java.") + || type.getPackageName().startsWith("jakarta.") + || type.getPackage().getName().startsWith("org.codehaus.groovy.vmplugin") + || skip.contains(type) + || skip.contains(type.getEnclosingClass()); + }).findFirst() + ); + } + + /** + * @param skip classes to skip in stack + * @return formatted caller source + */ + public static String getCallerSource(final List> skip) { + final StackWalker.StackFrame frame = getCaller(skip).orElse(null); + return "at " + (frame != null ? RenderUtils.renderPackage(frame.getDeclaringClass()) + + ".(" + frame.getFileName() + ":" + frame.getLineNumber() + ")" : "unknown source"); + } + + /** + * @return shared state calling source + */ + public static String getSharedStateSource() { + return getCallerSource(SHARED_STATE_INFRA); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java index 70cc4c3b2..c2a450e7e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceFeature.java @@ -11,9 +11,9 @@ import ru.vyarus.dropwizard.guice.module.jersey.hk2.InstallerBinder; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.LifecycleSupport; -import javax.inject.Provider; -import javax.ws.rs.core.Feature; -import javax.ws.rs.core.FeatureContext; +import jakarta.inject.Provider; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.JerseyTime; @@ -57,6 +57,14 @@ public class GuiceFeature implements Feature, Provider { private final boolean enableBridge; private InjectionManager injectionManager; + /** + * Create feature. + * + * @param provider injector provider + * @param tracker tracker + * @param lifecycle listeners support + * @param enableBridge true to enable hk guice bridge + */ public GuiceFeature(final Provider provider, final StatsTracker tracker, final LifecycleSupport lifecycle, final boolean enableBridge) { this.provider = provider; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java index 9229cffda..23f611ad9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/GuiceWebModule.java @@ -3,10 +3,11 @@ import com.google.inject.Stage; import com.google.inject.servlet.GuiceFilter; import com.google.inject.servlet.ServletModule; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.installer.internal.AdminGuiceFilter; -import javax.servlet.DispatcherType; +import jakarta.servlet.DispatcherType; + import java.util.EnumSet; /** @@ -19,6 +20,7 @@ * @author Vyacheslav Rusakov * @since 21.08.2016 */ +@SuppressWarnings("PMD.LooseCoupling") public class GuiceWebModule extends ServletModule { /** @@ -30,6 +32,12 @@ public class GuiceWebModule extends ServletModule { private final Environment environment; private final EnumSet dispatcherTypes; + /** + * Create web module. + * + * @param environment environment + * @param dispatcherTypes dispatcher types + */ public GuiceWebModule(final Environment environment, final EnumSet dispatcherTypes) { this.environment = environment; this.dispatcherTypes = dispatcherTypes; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java index 71762b681..ac831ada9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/Jersey2Module.java @@ -4,14 +4,15 @@ import com.google.common.base.Preconditions; import com.google.inject.AbstractModule; import com.google.inject.Stage; -import io.dropwizard.Application; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Environment; import org.glassfish.jersey.internal.inject.InjectionManager; import ru.vyarus.dropwizard.guice.injector.lookup.InjectorProvider; import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; import ru.vyarus.dropwizard.guice.module.jersey.hk2.GuiceBindingsModule; -import javax.servlet.DispatcherType; +import jakarta.servlet.DispatcherType; + import java.util.EnumSet; import static ru.vyarus.dropwizard.guice.GuiceyOptions.GuiceFilterRegistration; @@ -32,12 +33,20 @@ * @see ru.vyarus.dropwizard.guice.module.jersey.GuiceFeature for integration details * @since 31.08.2014 */ +@SuppressWarnings("PMD.LooseCoupling") public class Jersey2Module extends AbstractModule { private final Application application; private final Environment environment; private final ConfigurationContext context; + /** + * Create module. + * + * @param application application instance + * @param environment environment + * @param context configuration context + */ public Jersey2Module(final Application application, final Environment environment, final ConfigurationContext context) { this.application = application; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java similarity index 96% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java index 11a5aa134..0f3eaf53e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/HK2DebugBundle.java @@ -30,12 +30,12 @@ * Module intended to be used in tests. * {@link ru.vyarus.dropwizard.guice.module.jersey.debug.service.ContextDebugService} collects all tracked classes * instantiated by both guice and HK2 and may provide lists of classes accordingly. It may be used in test conditions. + *

      + * Soft deprecation: try to avoid hk2 direct usage if possible, someday HK2 support will be removed * * @author Vyacheslav Rusakov * @since 15.01.2016 - * @deprecated in the next version HK2 support will be removed and bundle will become useless */ -@Deprecated public class HK2DebugBundle extends UniqueGuiceyBundle { @Override diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java index 0a47e1ac0..d65b00735 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/ContextDebugService.java @@ -7,9 +7,10 @@ import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder; import ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding; -import javax.inject.Inject; -import javax.inject.Provider; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -37,6 +38,12 @@ public class ContextDebugService { private final Lock lock = new ReentrantLock(); private List> managedTypes; + /** + * Create jersey debug service. + * + * @param holder extensions holder + * @param options options + */ @Inject public ContextDebugService(final Provider holder, final Options options) { this.holder = holder; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java index 560242c9b..d8a2dcf50 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/GuiceInstanceListener.java @@ -3,7 +3,7 @@ import com.google.common.collect.Lists; import com.google.inject.spi.ProvisionListener; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.util.List; /** @@ -31,6 +31,11 @@ public void onProvision(final ProvisionInvocation provision) { } } + /** + * Inject debug service. + * + * @param contextDebugService debug service + */ @Inject public void setContextDebugService(final ContextDebugService contextDebugService) { this.contextDebugService = contextDebugService; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java similarity index 78% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java index a8c1c5c33..9ff8efac1 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2DebugFeature.java @@ -3,10 +3,10 @@ import org.glassfish.hk2.api.InstanceLifecycleListener; import org.glassfish.jersey.internal.inject.AbstractBinder; -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.ws.rs.core.Feature; -import javax.ws.rs.core.FeatureContext; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.core.Feature; +import jakarta.ws.rs.core.FeatureContext; /** * Jersey feature registers services instantiation tracker. @@ -19,6 +19,11 @@ public class HK2DebugFeature implements Feature { private final HK2InstanceListener listener; + /** + * Create debug feature. + * + * @param listener instance listener + */ @Inject public HK2DebugFeature(final HK2InstanceListener listener) { this.listener = listener; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java index 6d1d96c81..3a4487c36 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/HK2InstanceListener.java @@ -5,8 +5,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + import java.util.List; /** @@ -23,6 +24,11 @@ public class HK2InstanceListener implements InstanceLifecycleListener { private final ContextDebugService contextDebugService; + /** + * Create instance listener. + * + * @param contextDebugService context debug service + */ @Inject public HK2InstanceListener(final ContextDebugService contextDebugService) { this.contextDebugService = contextDebugService; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java similarity index 72% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java index b518dcead..f08d1c329 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/debug/service/WrongContextException.java @@ -8,6 +8,12 @@ */ public class WrongContextException extends RuntimeException { + /** + * Create exception. + * + * @param message message (with optional string format arguments) + * @param args message arguments + */ public WrongContextException(final String message, final Object... args) { super(String.format(message, args)); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java index 2298f9135..b78dc196c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBindingsModule.java @@ -8,25 +8,25 @@ import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; -import javax.inject.Provider; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.container.ResourceInfo; -import javax.ws.rs.core.*; -import javax.ws.rs.ext.Providers; +import jakarta.inject.Provider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.ext.Providers; import static ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent; /** * Registers important services from jersey context, making them available for injection in guice beans. *

        - *
      • {@link javax.ws.rs.core.Application} - *
      • {@link javax.ws.rs.ext.Providers} - *
      • {@link javax.ws.rs.core.UriInfo} - *
      • {@link javax.ws.rs.container.ResourceInfo} - *
      • {@link javax.ws.rs.core.HttpHeaders} - *
      • {@link javax.ws.rs.core.SecurityContext} - *
      • {@link javax.ws.rs.core.Request} + *
      • {@link jakarta.ws.rs.core.Application} + *
      • {@link jakarta.ws.rs.ext.Providers} + *
      • {@link jakarta.ws.rs.core.UriInfo} + *
      • {@link jakarta.ws.rs.container.ResourceInfo} + *
      • {@link jakarta.ws.rs.core.HttpHeaders} + *
      • {@link jakarta.ws.rs.core.SecurityContext} + *
      • {@link jakarta.ws.rs.core.Request} *
      • {@link org.glassfish.jersey.server.ContainerRequest} *
      • {@link org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider} *
      • {@link org.glassfish.jersey.server.AsyncContext}
      • @@ -46,6 +46,12 @@ public class GuiceBindingsModule extends AbstractModule { private final Provider provider; private final boolean guiceServletSupport; + /** + * Create bindings module. + * + * @param provider injector provider. + * @param guiceServletSupport true if guice servlets enabled + */ public GuiceBindingsModule(final Provider provider, final boolean guiceServletSupport) { this.provider = provider; this.guiceServletSupport = guiceServletSupport; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java index 3d08f4cec..8e68d17f6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/GuiceBridgeActivator.java @@ -21,6 +21,12 @@ public class GuiceBridgeActivator { private final InjectionManager injectionManager; private final Injector injector; + /** + * Create bridge activator. + * + * @param injectionManager hk injection manager + * @param injector guice injector + */ public GuiceBridgeActivator(final InjectionManager injectionManager, final Injector injector) { this.injectionManager = injectionManager; this.injector = injector; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java index 0e30d08e9..44143ad1c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/hk2/InstallerBinder.java @@ -29,6 +29,13 @@ public class InstallerBinder extends AbstractBinder { private final StatsTracker tracker; private final LifecycleSupport lifecycle; + /** + * Create binder. + * + * @param injector injector + * @param tracker tracker + * @param lifecycle listeners support + */ public InstallerBinder(final Injector injector, final StatsTracker tracker, final LifecycleSupport lifecycle) { this.injector = injector; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java index b741d9cd1..321a5b095 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/GuiceComponentFactory.java @@ -16,6 +16,12 @@ public class GuiceComponentFactory implements Supplier { private final Injector injector; private final Class type; + /** + * Create factory. + * + * @param injector injector + * @param type provided service type + */ public GuiceComponentFactory(final Injector injector, final Class type) { this.injector = injector; this.type = type; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java index 1abc061e9..ce7c5b26a 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/JerseyComponentProvider.java @@ -5,7 +5,7 @@ import org.glassfish.jersey.internal.inject.InjectionManager; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; -import javax.inject.Provider; +import jakarta.inject.Provider; /** * Lazy "bridge" used to register HK2 types in guice context. Guice context is created before HK2, @@ -21,6 +21,12 @@ public class JerseyComponentProvider implements Provider { private final Provider injector; private final Class type; + /** + * Create provider. + * + * @param injector injector provider + * @param type provided service type + */ public JerseyComponentProvider(final Provider injector, final Class type) { this.injector = injector; this.type = type; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java index 65a9bbe44..3d88223e9 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/jersey/support/LazyGuiceFactory.java @@ -18,6 +18,12 @@ public class LazyGuiceFactory implements Supplier { private final Injector injector; private final Class> type; + /** + * Create factory. + * + * @param injector injector + * @param type original factory + */ public LazyGuiceFactory(final Injector injector, final Class> type) { this.injector = injector; this.type = type; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java index bbb843aef..4951195c5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycle.java @@ -24,6 +24,14 @@ public enum GuiceyLifecycle { * Provides all instances of executed hooks. Not called if no hooks used. */ ConfigurationHooksProcessed(ConfigurationHooksProcessedEvent.class), + /** + * Special meta event, called before all {@link ru.vyarus.dropwizard.guice.GuiceBundle} configuration phase logic. + * {@link io.dropwizard.core.setup.Bootstrap} object is available, but dropwizard bundles (registered through + * guicey) are not yet registered (note that {@link ru.vyarus.dropwizard.guice.GuiceBundle} is not yet added to + * bootstrap also because dropwizard calls bundle initialization before registering bundle (and so all dropwizard + * bundles, registered by guicey, will run before {@link ru.vyarus.dropwizard.guice.GuiceBundle} run). + */ + BeforeInit(BeforeInitEvent.class), /** * Called after dropwizard bundles initialization (for dropwizard bundles registered through guicey api). * Not called if no bundles were registered. @@ -57,12 +65,6 @@ public enum GuiceyLifecycle { * Called even if no installers are resolved to indicate configuration state. */ InstallersResolved(InstallersResolvedEvent.class), - /** - * Called when all manually registered extension classes are recognized by installers (validated). But only - * extensions, known to be enabled at that time are actually validated (this way it is possible to exclude - * extensions for non existing installers). Called only if at least one manual extension registered. - */ - ManualExtensionsValidated(ManualExtensionsValidatedEvent.class), /** * Called when classes from classpath scan analyzed and all extensions detected. * Called only if classpath scan is enabled and at least one extension detected. @@ -93,6 +95,14 @@ public enum GuiceyLifecycle { * guice bundle processing. */ BundlesStarted(BundlesStartedEvent.class), + /** + * Called when all manually registered extension classes are recognized by installers (validated). But only + * extensions, known to be enabled at that time are actually validated (this way it is possible to exclude + * extensions for non existing installers). Called only if at least one manual extension registered. + *

        + * Performed in run phase because extensions could be registered in both phases. + */ + ManualExtensionsValidated(ManualExtensionsValidatedEvent.class), /** * Called after guice modules analysis and repackaging. Reveals all detected extensions and removed bindings info. * Called only if bindings analysis is enabled. @@ -133,7 +143,8 @@ public enum GuiceyLifecycle { ExtensionsInstalled(ExtensionsInstalledEvent.class), /** * Called after - * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(io.dropwizard.Configuration, io.dropwizard.setup.Environment)} + * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)} * when guicey context is started, extensions installed (but not hk extensions, because neither jersey nor jetty * is't start yet). *

        @@ -141,13 +152,31 @@ public enum GuiceyLifecycle { * run application instead of "server"). Injector itself is completely initialized - all singletons started. *

        * This point is before - * {@link io.dropwizard.Application#run(io.dropwizard.Configuration, io.dropwizard.setup.Environment)}. Ideal point - * for jersey and jetty listeners installation (with shortcut event methods). + * {@link io.dropwizard.core.Application#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)}. Ideal point for jersey and jetty + * listeners installation (with shortcut event methods). To run after application run use + * {@link #ApplicationStarting}. */ ApplicationRun(ApplicationRunEvent.class), // -- Application.run() + /** + * Called after complete application configuration ({@link io.dropwizard.core.Application#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)} called), but before lifecycle + * startup (before managed objects run). Actually the same as jetty lifecycle started event + * ({@link org.eclipse.jetty.util.component.LifeCycle.Listener#lifeCycleStarting( + * org.eclipse.jetty.util.component.LifeCycle)}. + *

        + * May be used as for additional services startup (after all initializations), executed before "started" point, + * often used for reporting. As an example, sub rest use this event to run jersey context after initialization. + * This event also will be fired in guicey tests ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp} + * which does not start the web part). + *

        + * NOTE: jersey context is not started yet! + */ + ApplicationStarting(ApplicationStartingEvent.class), + /** * Jersey context starting. At this point jersey is starting and jetty is only initializing. Since that point * jersey {@link org.glassfish.jersey.internal.inject.InjectionManager} is accessible. @@ -196,7 +225,7 @@ public enum GuiceyLifecycle { *

        * May be used to perform some shutdown logic. */ - ApplicationShutdown(ApplicationShotdownEvent.class), + ApplicationShutdown(ApplicationShutdownEvent.class), /** * Called after application shutdown. Triggered by jetty lifecycle stopping event ( * {@link org.eclipse.jetty.util.component.LifeCycle.Listener#lifeCycleStopped( diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java index afd8687e7..b4d7b8de7 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleAdapter.java @@ -1,6 +1,7 @@ package ru.vyarus.dropwizard.guice.module.lifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.event.GuiceyLifecycleEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BeforeInitEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesFromLookupResolvedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesInitializedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesResolvedEvent; @@ -11,8 +12,9 @@ import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InitializedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InstallersResolvedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.ManualExtensionsValidatedEvent; -import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShotdownEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShutdownEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartingEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.JerseyConfigurationEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.JerseyExtensionsInstalledByEvent; @@ -33,18 +35,21 @@ * @author Vyacheslav Rusakov * @since 18.04.2018 */ -@SuppressWarnings({"checkstyle:ClassFanOutComplexity", "PMD.TooManyMethods"}) +@SuppressWarnings({"checkstyle:ClassFanOutComplexity", "PMD.TooManyMethods", "PMD.CouplingBetweenObjects"}) public class GuiceyLifecycleAdapter implements GuiceyLifecycleListener { @Override @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:MissingSwitchDefault", "checkstyle:JavaNCSS", - "PMD.NcssCount", "PMD.CyclomaticComplexity", "PMD.SwitchStmtsShouldHaveDefault"}) + "PMD.NcssCount", "PMD.CyclomaticComplexity"}) public void onEvent(final GuiceyLifecycleEvent event) { switch (event.getType()) { case ConfigurationHooksProcessed: configurationHooksProcessed((ConfigurationHooksProcessedEvent) event); break; + case BeforeInit: + beforeInit((BeforeInitEvent) event); + break; case DropwizardBundlesInitialized: dropwizardBundlesInitialized((DropwizardBundlesInitializedEvent) event); break; @@ -63,9 +68,6 @@ public void onEvent(final GuiceyLifecycleEvent event) { case InstallersResolved: installersResolved((InstallersResolvedEvent) event); break; - case ManualExtensionsValidated: - manualExtensionsValidated((ManualExtensionsValidatedEvent) event); - break; case ClasspathExtensionsResolved: classpathExtensionsResolved((ClasspathExtensionsResolvedEvent) event); break; @@ -78,6 +80,9 @@ public void onEvent(final GuiceyLifecycleEvent event) { case BundlesStarted: bundlesStarted((BundlesStartedEvent) event); break; + case ManualExtensionsValidated: + manualExtensionsValidated((ManualExtensionsValidatedEvent) event); + break; case ModulesAnalyzed: modulesAnalyzed((ModulesAnalyzedEvent) event); break; @@ -96,6 +101,9 @@ public void onEvent(final GuiceyLifecycleEvent event) { case ApplicationRun: applicationRun((ApplicationRunEvent) event); break; + case ApplicationStarting: + applicationStarting((ApplicationStartingEvent) event); + break; case JerseyConfiguration: jerseyConfiguration((JerseyConfigurationEvent) event); break; @@ -109,7 +117,7 @@ public void onEvent(final GuiceyLifecycleEvent event) { applicationStarted((ApplicationStartedEvent) event); break; case ApplicationShutdown: - applicationShutdown((ApplicationShotdownEvent) event); + applicationShutdown((ApplicationShutdownEvent) event); break; case ApplicationStopped: applicationStopped((ApplicationStoppedEvent) event); @@ -129,6 +137,20 @@ protected void configurationHooksProcessed(final ConfigurationHooksProcessedEven // empty } + /** + * Special meta event, called before all {@link ru.vyarus.dropwizard.guice.GuiceBundle} configuration phase logic. + * {@link io.dropwizard.core.setup.Bootstrap} object is available, but dropwizard bundles (registered through + * guicey) are not yet registered (note that {@link ru.vyarus.dropwizard.guice.GuiceBundle} is not yet added to + * bootstrap also because dropwizard calls bundle initialization before registering bundle (and so all dropwizard + * bundles, registered by guicey, will run before {@link ru.vyarus.dropwizard.guice.GuiceBundle} run). + * + * @param event event object + * @see GuiceyLifecycle#BeforeInit + */ + protected void beforeInit(final BeforeInitEvent event) { + // empty + } + /** * Called after dropwizard bundles initialization (for dropwizard bundles registered through guicey api). * Not called if no bundles were registered. @@ -195,18 +217,6 @@ protected void installersResolved(final InstallersResolvedEvent event) { // empty } - /** - * Called when all manually registered extension classes are recognized by installers (validated). But only - * extensions, known to be enabled at that time are actually validated (this way it is possible to exclude - * extensions for non existing installers). Called only if at least one manual extension registered. - * - * @param event event object - * @see GuiceyLifecycle#ManualExtensionsValidated - */ - protected void manualExtensionsValidated(final ManualExtensionsValidatedEvent event) { - // empty - } - /** * Called when classes from classpath scan analyzed and all extensions detected. Called only if classpath scan * is enabled and at least one extension detected. @@ -250,6 +260,18 @@ protected void bundlesStarted(final BundlesStartedEvent event) { // empty } + /** + * Called when all manually registered extension classes are recognized by installers (validated). But only + * extensions, known to be enabled at that time are actually validated (this way it is possible to exclude + * extensions for non existing installers). Called only if at least one manual extension registered. + * + * @param event event object + * @see GuiceyLifecycle#ManualExtensionsValidated + */ + protected void manualExtensionsValidated(final ManualExtensionsValidatedEvent event) { + // empty + } + /** * Called when guice bindings analyzed and all extensions detected. Called only if bindings analysis is enabled. * @@ -305,7 +327,8 @@ protected void extensionsInstalled(final ExtensionsInstalledEvent event) { /** * Called after - * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(io.dropwizard.Configuration, io.dropwizard.setup.Environment)} + * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(io.dropwizard.core.Configuration, + * io.dropwizard.core.setup.Environment)} * when guicey context is started, extensions installed (but not hk extensions, because neither jersey nor jetty * isn't start yet). * @@ -316,6 +339,20 @@ protected void applicationRun(final ApplicationRunEvent event) { // empty } + /** + * Called after complete application configuration ({@link io.dropwizard.core.Application#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)} called), but before lifecycle + * startup (before managed objects run). Actually the same as jetty lifecycle started event + * ({@link org.eclipse.jetty.util.component.LifeCycle.Listener#lifeCycleStarting( + * org.eclipse.jetty.util.component.LifeCycle)}. + * + * @param event event object + * @see GuiceyLifecycle#ApplicationStarting + */ + protected void applicationStarting(final ApplicationStartingEvent event) { + // empty + } + /** * Jersey context starting. At this point jersey and jetty is only initializing. Guicey jersey configuration * is not yer performed. Since that point jersey {@link org.glassfish.jersey.internal.inject.InjectionManager} @@ -370,7 +407,7 @@ protected void applicationStarted(final ApplicationStartedEvent event) { * @param event event object * @see GuiceyLifecycle#ApplicationShutdown */ - protected void applicationShutdown(final ApplicationShotdownEvent event) { + protected void applicationShutdown(final ApplicationShutdownEvent event) { // empty } @@ -378,7 +415,6 @@ protected void applicationShutdown(final ApplicationShotdownEvent event) { * Called after application shutdown. Triggered by jetty lifecycle stopping event ( * {@link org.eclipse.jetty.util.component.LifeCycle.Listener#lifeCycleStopped( * org.eclipse.jetty.util.component.LifeCycle)}). - *

        * * @param event event object * @see GuiceyLifecycle#ApplicationStopped diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleListener.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/GuiceyLifecycleListener.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/UniqueGuiceyLifecycleListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/UniqueGuiceyLifecycleListener.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/UniqueGuiceyLifecycleListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/UniqueGuiceyLifecycleListener.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java similarity index 83% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java index 93ef1bf1b..3c92a2bb5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/ConfigurationPhaseEvent.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.event; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.setup.Bootstrap; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; @@ -14,6 +14,12 @@ public abstract class ConfigurationPhaseEvent extends GuiceyLifecycleEvent { private final Bootstrap bootstrap; + /** + * Create event. + * + * @param type event type + * @param context event context + */ public ConfigurationPhaseEvent(final GuiceyLifecycle type, final EventsContext context) { super(type, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java index 00d1b6eec..b76a9f43e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/GuiceyLifecycleEvent.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsInfo; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; @@ -14,7 +15,7 @@ * {@link ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.ConfigurationHooksProcessedEvent}) simply * don't have access for it. *

      • {@link ConfigurationPhaseEvent} - dropwizard configuration phase events (all have access to dropwizard - * {@link io.dropwizard.setup.Bootstrap} object)
      • + * {@link io.dropwizard.core.setup.Bootstrap} object) *
      • {@link RunPhaseEvent} - events started on dropwizard run phase (when configuration is available)
      • *
      • {@link InjectorPhaseEvent} - all events after guice injector creation
      • *
      • {@link JerseyPhaseEvent} - all events after jersey context initialization start (since @@ -27,15 +28,22 @@ public abstract class GuiceyLifecycleEvent { private final GuiceyLifecycle type; + private final StatsInfo stats; private final Options options; private final SharedConfigurationState sharedState; - + /** + * Create event. + * + * @param type event type + * @param context event context + */ public GuiceyLifecycleEvent(final GuiceyLifecycle type, final EventsContext context) { Preconditions.checkState(type.getType().equals(getClass()), "Wrong event type %s used for class %s", type, getClass().getSimpleName()); this.type = type; + this.stats = new StatsInfo(context.getTracker()); this.options = context.getOptions(); this.sharedState = context.getSharedState(); } @@ -59,6 +67,13 @@ public Options getOptions() { return options; } + /** + * @return tracked stats + */ + public StatsInfo getStats() { + return stats; + } + /** * @return application shared state * @see SharedConfigurationState diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java similarity index 97% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java index e52786a79..717464994 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/InjectorPhaseEvent.java @@ -30,6 +30,12 @@ public abstract class InjectorPhaseEvent extends RunPhaseEvent { private final Injector injector; private final ReportRenderer reportRenderer = new ReportRenderer(); + /** + * Create event. + * + * @param type event type + * @param context event context + */ public InjectorPhaseEvent(final GuiceyLifecycle type, final EventsContext context) { super(type, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java similarity index 78% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java index 031b92442..4ba599d7f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/JerseyPhaseEvent.java @@ -16,6 +16,12 @@ public abstract class JerseyPhaseEvent extends InjectorPhaseEvent { private final InjectionManager injectionManager; + /** + * Create event. + * + * @param type event type + * @param context event context + */ public JerseyPhaseEvent(final GuiceyLifecycle type, final EventsContext context) { super(type, context); @@ -35,4 +41,13 @@ public JerseyPhaseEvent(final GuiceyLifecycle type, public InjectionManager getInjectionManager() { return injectionManager; } + + /** + * Jersey (rest) could start without jetty (web) only in lightweight guicey tests (rest stubs). + * + * @return true if jersey started, false otherwise + */ + public boolean isJerseyStarted() { + return getInjectionManager() != null; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java similarity index 87% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java index 3462756d9..f8dd1fb5f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/RunPhaseEvent.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.event; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig; import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; @@ -10,7 +10,7 @@ /** * Base class for events, started after {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(Configuration, Environment)} - * phase. Most events will appear before ({@link io.dropwizard.Application#run(Configuration, Environment)}). + * phase. Most events will appear before ({@link io.dropwizard.core.Application#run(Configuration, Environment)}). * * @author Vyacheslav Rusakov * @since 19.04.2018 @@ -21,6 +21,12 @@ public abstract class RunPhaseEvent extends ConfigurationPhaseEvent { private final ConfigurationTree configurationTree; private final Environment environment; + /** + * Create event. + * + * @param type event type + * @param context event context + */ public RunPhaseEvent(final GuiceyLifecycle type, final EventsContext context) { super(type, context); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BeforeInitEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BeforeInitEvent.java new file mode 100644 index 000000000..719c42112 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BeforeInitEvent.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration; + +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.ConfigurationPhaseEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; + +/** + * Special meta event, called before all {@link ru.vyarus.dropwizard.guice.GuiceBundle} configuration phase logic. + * {@link io.dropwizard.core.setup.Bootstrap} object is available, but dropwizard bundles (registered through + * guicey) are not yet registered (note that {@link ru.vyarus.dropwizard.guice.GuiceBundle} is not yet added to + * bootstrap also because dropwizard calls bundle initialization before registering bundle (and so all dropwizard + * bundles, registered by guicey, will run before {@link ru.vyarus.dropwizard.guice.GuiceBundle} run). + * + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class BeforeInitEvent extends ConfigurationPhaseEvent { + + /** + * Create event. + * + * @param context event context + */ + public BeforeInitEvent(final EventsContext context) { + super(GuiceyLifecycle.BeforeInit, context); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java index 03b004448..5f25ec7ec 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesFromLookupResolvedEvent.java @@ -20,6 +20,12 @@ public class BundlesFromLookupResolvedEvent extends ConfigurationPhaseEvent { private final List bundles; + /** + * Create event. + * + * @param context event context + * @param bundles guicey bundles + */ public BundlesFromLookupResolvedEvent(final EventsContext context, final List bundles) { super(GuiceyLifecycle.BundlesFromLookupResolved, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java index c3e71cfae..3ae9c06bf 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesInitializedEvent.java @@ -24,6 +24,14 @@ public class BundlesInitializedEvent extends ConfigurationPhaseEvent { private final List disabled; private final List ignored; + /** + * Create event. + * + * @param context event context + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicate registrations) + */ public BundlesInitializedEvent(final EventsContext context, final List bundles, final List disabled, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java index 6ba30d472..51e4c6a0f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/BundlesResolvedEvent.java @@ -24,6 +24,14 @@ public class BundlesResolvedEvent extends ConfigurationPhaseEvent { private final List disabled; private final List ignored; + /** + * Create event. + * + * @param context event context + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicates) + */ public BundlesResolvedEvent(final EventsContext context, final List bundles, final List disabled, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java index c3d0c033c..db1e116fa 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ClasspathExtensionsResolvedEvent.java @@ -20,6 +20,12 @@ public class ClasspathExtensionsResolvedEvent extends ConfigurationPhaseEvent { private final List> extensions; + /** + * Create event. + * + * @param context event context + * @param extensions extensions + */ public ClasspathExtensionsResolvedEvent(final EventsContext context, final List> extensions) { super(GuiceyLifecycle.ClasspathExtensionsResolved, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java index 99adfcc86..5906517d3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/CommandsResolvedEvent.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration; -import io.dropwizard.cli.Command; +import io.dropwizard.core.cli.Command; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.event.ConfigurationPhaseEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; @@ -18,6 +18,12 @@ public class CommandsResolvedEvent extends ConfigurationPhaseEvent { private final List commands; + /** + * Create event. + * + * @param context event context + * @param installed installed commands + */ public CommandsResolvedEvent(final EventsContext context, final List installed) { super(GuiceyLifecycle.CommandsResolved, context); commands = installed; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java similarity index 84% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java index d1dc6fa61..3ac9adf4b 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ConfigurationHooksProcessedEvent.java @@ -13,7 +13,7 @@ *

        * Provides list of all used hooks. *

        - * Note: dropwizard {@link io.dropwizard.setup.Bootstrap} object is already existing at that moment, but bundle + * Note: dropwizard {@link io.dropwizard.core.setup.Bootstrap} object is already existing at that moment, but bundle * don't have access for it yet and so it's not available in event. * * @author Vyacheslav Rusakov @@ -23,6 +23,12 @@ public class ConfigurationHooksProcessedEvent extends GuiceyLifecycleEvent { private final Set hooks; + /** + * Create event. + * + * @param context event context + * @param hooks hooks + */ public ConfigurationHooksProcessedEvent(final EventsContext context, final Set hooks) { super(GuiceyLifecycle.ConfigurationHooksProcessed, context); this.hooks = hooks; diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java index f463db4f1..3ee2a4eca 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/DropwizardBundlesInitializedEvent.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration; -import io.dropwizard.ConfiguredBundle; +import io.dropwizard.core.ConfiguredBundle; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.event.ConfigurationPhaseEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; @@ -23,6 +23,14 @@ public class DropwizardBundlesInitializedEvent extends ConfigurationPhaseEvent { private final List disabled; private final List ignored; + /** + * Create event. + * + * @param context event context + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicates) + */ public DropwizardBundlesInitializedEvent(final EventsContext context, final List bundles, final List disabled, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java index 92023338d..46e767c13 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InitializedEvent.java @@ -17,6 +17,11 @@ */ public class InitializedEvent extends ConfigurationPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public InitializedEvent(final EventsContext context) { super(GuiceyLifecycle.Initialized, context); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java index 978064d14..299885359 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/InstallersResolvedEvent.java @@ -20,6 +20,13 @@ public class InstallersResolvedEvent extends ConfigurationPhaseEvent { private final List installers; private final List> disabled; + /** + * Create event. + * + * @param context event context + * @param installers installers + * @param disabled disabled installers + */ public InstallersResolvedEvent(final EventsContext context, final List installers, final List> disabled) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java index 0d7d91d2a..69bdb5910 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/configuration/ManualExtensionsValidatedEvent.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; -import ru.vyarus.dropwizard.guice.module.lifecycle.event.ConfigurationPhaseEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.RunPhaseEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; import java.util.List; @@ -18,11 +18,18 @@ * @author Vyacheslav Rusakov * @since 01.09.2019 */ -public class ManualExtensionsValidatedEvent extends ConfigurationPhaseEvent { +public class ManualExtensionsValidatedEvent extends RunPhaseEvent { private final List> extensions; private final List> validated; + /** + * Create event. + * + * @param context event context + * @param extensions all extensions + * @param validated manual extensions + */ public ManualExtensionsValidatedEvent(final EventsContext context, final List> extensions, final List> validated) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShotdownEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShutdownEvent.java similarity index 85% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShotdownEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShutdownEvent.java index 0b38bf694..e9932bda5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShotdownEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationShutdownEvent.java @@ -15,9 +15,14 @@ * @author Vyacheslav Rusakov * @since 25.10.2019 */ -public class ApplicationShotdownEvent extends JerseyPhaseEvent { +public class ApplicationShutdownEvent extends JerseyPhaseEvent { - public ApplicationShotdownEvent(final EventsContext context) { + /** + * Create event. + * + * @param context event context + */ + public ApplicationShutdownEvent(final EventsContext context) { super(GuiceyLifecycle.ApplicationShutdown, context); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java similarity index 95% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java index 04e44f604..e52ddd4cf 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartedEvent.java @@ -21,6 +21,11 @@ */ public class ApplicationStartedEvent extends JerseyPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public ApplicationStartedEvent(final EventsContext context) { super(GuiceyLifecycle.ApplicationStarted, context); } @@ -48,7 +53,7 @@ public boolean isJettyStarted() { */ public String renderJerseyConfig(final JerseyConfig config) { final Boolean guiceFirstMode = getOptions().get(InstallersOptions.JerseyExtensionsManagedByGuice); - return isJettyStarted() + return isJerseyStarted() ? new JerseyConfigRenderer(getInjectionManager(), guiceFirstMode).renderReport(config) : ""; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartingEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartingEvent.java new file mode 100644 index 000000000..e8635b676 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStartingEvent.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey; + +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.JerseyPhaseEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.internal.EventsContext; + +/** + * Called after complete application configuration ({@link io.dropwizard.core.Application#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)} called), but before lifecycle startup + * (before managed objects run). Actually the same as jetty lifecycle started event + * ({@link org.eclipse.jetty.util.component.LifeCycle.Listener#lifeCycleStarting( + * org.eclipse.jetty.util.component.LifeCycle)}. + *

        + * May be used as for additional services startup (after all initializations), executed before "started" point, + * often used for reporting. As an example, sub rest use this event to run jersey context after initialization. + * This event also will be fired in guicey tests ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp} + * which does not start the web part). + * + * @author Vyacheslav Rusakov + * @since 16.10.2025 + */ +public class ApplicationStartingEvent extends JerseyPhaseEvent { + + /** + * Create event. + * + * @param context even context + */ + public ApplicationStartingEvent(final EventsContext context) { + super(GuiceyLifecycle.ApplicationStarting, context); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java index 388b7f22c..6f49c8939 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/ApplicationStoppedEvent.java @@ -16,6 +16,11 @@ */ public class ApplicationStoppedEvent extends JerseyPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public ApplicationStoppedEvent(final EventsContext context) { super(GuiceyLifecycle.ApplicationStopped, context); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java index 5eb6bbc97..4bde39500 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyConfigurationEvent.java @@ -14,6 +14,11 @@ */ public class JerseyConfigurationEvent extends JerseyPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public JerseyConfigurationEvent(final EventsContext context) { super(GuiceyLifecycle.JerseyConfiguration, context); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java index b1c318e97..a6a6fa040 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledByEvent.java @@ -31,6 +31,13 @@ public class JerseyExtensionsInstalledByEvent extends JerseyPhaseEvent { private final Class installer; private final List> installed; + /** + * Create event. + * + * @param context event context + * @param installer installer type + * @param installed installed extensions + */ public JerseyExtensionsInstalledByEvent(final EventsContext context, final Class installer, final List> installed) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java index 9284ae82b..4e30ee7b2 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/jersey/JerseyExtensionsInstalledEvent.java @@ -27,6 +27,12 @@ public class JerseyExtensionsInstalledEvent extends JerseyPhaseEvent { private final List> extensions; + /** + * Create event. + * + * @param context event context + * @param extensions installed extensions + */ @SuppressWarnings("checkstyle:ParameterNumber") public JerseyExtensionsInstalledEvent(final EventsContext context, final List> extensions) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java similarity index 75% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java index 46e292b2b..7c5fccbca 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ApplicationRunEvent.java @@ -8,7 +8,8 @@ /** * Called after - * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run(io.dropwizard.Configuration, io.dropwizard.setup.Environment)} + * {@link ru.vyarus.dropwizard.guice.GuiceBundle#run( + * io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)} * when guicey context is started, extensions installed (but not hk extensions, because neither jersey nor jetty * isn't start yet). *

        @@ -16,8 +17,8 @@ * run application instead of "server"). Injector itself is completely initialized - all singletons processed. *

        * This point is before - * {@link io.dropwizard.Application#run(io.dropwizard.Configuration, io.dropwizard.setup.Environment)}. Ideal point - * for jersey and jetty listeners installation (use shortcut methods in event for registration). + * {@link io.dropwizard.core.Application#run(io.dropwizard.core.Configuration, io.dropwizard.core.setup.Environment)}. + * Ideal point for jersey and jetty listeners installation (use shortcut methods in event for registration). * * @author Vyacheslav Rusakov * @see ru.vyarus.dropwizard.guice.debug.LifecycleDiagnostic for listeners usage example @@ -25,6 +26,11 @@ */ public class ApplicationRunEvent extends InjectorPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public ApplicationRunEvent(final EventsContext context) { super(GuiceyLifecycle.ApplicationRun, context); } @@ -33,7 +39,7 @@ public ApplicationRunEvent(final EventsContext context) { * @param listener jetty listener */ public void registerJettyListener(final LifeCycle.Listener listener) { - getEnvironment().lifecycle().addLifeCycleListener(listener); + getEnvironment().lifecycle().addEventListener(listener); } /** diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java similarity index 88% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java index be95ecef4..b54246e02 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BeforeRunEvent.java @@ -12,6 +12,11 @@ */ public class BeforeRunEvent extends RunPhaseEvent { + /** + * Create event. + * + * @param context event context + */ public BeforeRunEvent(final EventsContext context) { super(GuiceyLifecycle.BeforeRun, context); } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java index 324f1cbcc..e76ee4d66 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/BundlesStartedEvent.java @@ -19,6 +19,12 @@ public class BundlesStartedEvent extends RunPhaseEvent { private final List bundles; + /** + * Create event. + * + * @param context event context + * @param bundles bundles + */ public BundlesStartedEvent(final EventsContext context, final List bundles) { super(GuiceyLifecycle.BundlesStarted, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java index 02f60cfe1..2111ec9d3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledByEvent.java @@ -29,6 +29,13 @@ public class ExtensionsInstalledByEvent extends InjectorPhaseEvent { private final Class installer; private final List> installed; + /** + * Create event. + * + * @param context event context + * @param installer installer type + * @param installed installed extensions + */ public ExtensionsInstalledByEvent(final EventsContext context, final Class installer, final List> installed) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java index a80ed19ea..15d81e26f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsInstalledEvent.java @@ -19,6 +19,12 @@ public class ExtensionsInstalledEvent extends InjectorPhaseEvent { private final List> extensions; + /** + * Create event. + * + * @param context event context + * @param extensions installed extensions + */ public ExtensionsInstalledEvent(final EventsContext context, final List> extensions) { super(GuiceyLifecycle.ExtensionsInstalled, context); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java index 375c8933b..b8cd68aa5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ExtensionsResolvedEvent.java @@ -20,6 +20,13 @@ public class ExtensionsResolvedEvent extends RunPhaseEvent { private final List> extensions; private final List> disabled; + /** + * Create event. + * + * @param context event context + * @param extensions actual extensions + * @param disabled disabled extensions + */ public ExtensionsResolvedEvent(final EventsContext context, final List> extensions, final List> disabled) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java index 3c02b5b71..653da7a3a 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/InjectorCreationEvent.java @@ -23,6 +23,15 @@ public class InjectorCreationEvent extends RunPhaseEvent { private final List disabled; private final List ignored; + /** + * Create event. + * + * @param context event context + * @param modules modules + * @param overriding overriding modules + * @param disabled disabled modules + * @param ignored ignored modules (duplicates) + */ public InjectorCreationEvent(final EventsContext context, final List modules, final List overriding, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java index c4e4fea4f..e90df03bb 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/event/run/ModulesAnalyzedEvent.java @@ -25,6 +25,15 @@ public class ModulesAnalyzedEvent extends RunPhaseEvent { private final List> transitiveModulesRemoved; private final List bindingsRemoved; + /** + * Create event. + * + * @param context event context + * @param analyzedModules modules + * @param extensions resolved extensions + * @param transitiveModulesRemoved removed modules + * @param bindingsRemoved removed bindings + */ @SuppressWarnings("checkstyle:ParameterNumber") public ModulesAnalyzedEvent(final EventsContext context, final List analyzedModules, diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java similarity index 61% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java index 6d608f562..f38e7be12 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/EventsContext.java @@ -1,12 +1,13 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.internal; import com.google.inject.Injector; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.glassfish.jersey.internal.inject.InjectionManager; import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; /** @@ -17,6 +18,7 @@ */ public class EventsContext { + private final StatsTracker tracker; private final Options options; private final SharedConfigurationState sharedState; private Bootstrap bootstrap; @@ -26,63 +28,122 @@ public class EventsContext { private Injector injector; private InjectionManager injectionManager; - public EventsContext(final Options options, final SharedConfigurationState sharedState) { + /** + * Create events context. + * + * @param tracker tracker + * @param options options + * @param sharedState shared state + */ + public EventsContext(final StatsTracker tracker, + final Options options, + final SharedConfigurationState sharedState) { + this.tracker = tracker; this.options = options; this.sharedState = sharedState; } + /** + * @param bootstrap bootstrap + */ public void setBootstrap(final Bootstrap bootstrap) { this.bootstrap = bootstrap; } + /** + * @param configuration configuration + */ public void setConfiguration(final Configuration configuration) { this.configuration = configuration; } + /** + * @param configurationTree parsed configuration + */ public void setConfigurationTree(final ConfigurationTree configurationTree) { this.configurationTree = configurationTree; } + /** + * @param environment environment + */ public void setEnvironment(final Environment environment) { this.environment = environment; } + /** + * @param injector injector + */ public void setInjector(final Injector injector) { this.injector = injector; } + /** + * @param injectionManager injection manager + */ public void setInjectionManager(final InjectionManager injectionManager) { this.injectionManager = injectionManager; } + /** + * @return tracker + */ + public StatsTracker getTracker() { + return tracker; + } + + /** + * @return options + */ public Options getOptions() { return options; } + /** + * @return shared state + */ public SharedConfigurationState getSharedState() { return sharedState; } + /** + * @return bootstrap + */ public Bootstrap getBootstrap() { return bootstrap; } + /** + * @return configuration + */ public Configuration getConfiguration() { return configuration; } + /** + * @return parsed configuration + */ public ConfigurationTree getConfigurationTree() { return configurationTree; } + /** + * @return environment + */ public Environment getEnvironment() { return environment; } + /** + * @return injector + */ public Injector getInjector() { return injector; } + /** + * @return jersey injection manager + */ public InjectionManager getInjectionManager() { return injectionManager; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java similarity index 64% rename from src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java index 001654f38..f177649a4 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/lifecycle/internal/LifecycleSupport.java @@ -1,13 +1,14 @@ package ru.vyarus.dropwizard.guice.module.lifecycle.internal; +import com.google.common.base.Stopwatch; import com.google.inject.Binding; import com.google.inject.Injector; import com.google.inject.Module; -import io.dropwizard.Configuration; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.cli.Command; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.eclipse.jetty.util.component.LifeCycle; import org.glassfish.jersey.internal.inject.InjectionManager; import org.slf4j.Logger; @@ -15,11 +16,16 @@ import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.context.stat.DetailStat; +import ru.vyarus.dropwizard.guice.module.context.stat.Stat; +import ru.vyarus.dropwizard.guice.module.context.stat.StatTimer; +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; import ru.vyarus.dropwizard.guice.module.lifecycle.event.GuiceyLifecycleEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BeforeInitEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesFromLookupResolvedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesInitializedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BundlesResolvedEvent; @@ -30,8 +36,9 @@ import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InitializedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InstallersResolvedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.ManualExtensionsValidatedEvent; -import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShotdownEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShutdownEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartingEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.JerseyConfigurationEvent; import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.JerseyExtensionsInstalledByEvent; @@ -58,19 +65,37 @@ * @since 17.04.2018 */ @SuppressWarnings({"checkstyle:ClassDataAbstractionCoupling", "checkstyle:ClassFanOutComplexity", - "PMD.ExcessiveImports", "PMD.TooManyMethods"}) + "PMD.ExcessiveImports", "PMD.TooManyMethods", "PMD.CouplingBetweenObjects"}) public final class LifecycleSupport { private final Logger logger = LoggerFactory.getLogger(LifecycleSupport.class); + private final StatsTracker tracker; private final EventsContext context; + private final Runnable startupHook; private GuiceyLifecycle currentStage; private final Set listeners = new LinkedHashSet<>(); - public LifecycleSupport(final Options options, final SharedConfigurationState sharedState) { - this.context = new EventsContext(options, sharedState); + /** + * Create support. + * + * @param tracker stats tracker + * @param options options + * @param sharedState shared state + * @param startupHook startup hook + */ + public LifecycleSupport(final StatsTracker tracker, final Options options, + final SharedConfigurationState sharedState, final Runnable startupHook) { + this.tracker = tracker; + this.context = new EventsContext(tracker, options, sharedState); + this.startupHook = startupHook; } + /** + * Listener registration. + * + * @param listeners lifecycle listener + */ public void register(final GuiceyLifecycleListener... listeners) { Arrays.asList(listeners).forEach(l -> { if (!this.listeners.add(l)) { @@ -79,34 +104,73 @@ public void register(final GuiceyLifecycleListener... listeners) { }); } + /** + * Hooks processed. + * + * @param hooks processed hooks + */ public void configurationHooksProcessed(final Set hooks) { if (hooks != null && !hooks.isEmpty()) { broadcast(new ConfigurationHooksProcessedEvent(context, hooks)); } } - public void initializationStarted(final Bootstrap bootstrap, - final List bundles, - final List disabled, - final List ignored) { + /** + * Before gucie bundle initialization. + * + * @param bootstrap bootstrap + */ + public void beforeInit(final Bootstrap bootstrap) { this.context.setBootstrap(bootstrap); + broadcast(new BeforeInitEvent(context)); + } + + /** + * Dropwizard bundles initialized. + * + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicates) + */ + public void dropwizardBundlesInitialized(final List bundles, + final List disabled, + final List ignored) { if (!bundles.isEmpty()) { broadcast(new DropwizardBundlesInitializedEvent(context, bundles, disabled, ignored)); } } + /** + * Bundles from lookup resolved. + * + * @param bundles resolved bundles + */ public void bundlesFromLookupResolved(final List bundles) { if (!bundles.isEmpty()) { broadcast(new BundlesFromLookupResolvedEvent(context, bundles)); } } + /** + * All bundles resolved. + * + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicates) + */ public void bundlesResolved(final List bundles, final List disabled, final List ignored) { broadcast(new BundlesResolvedEvent(context, bundles, disabled, ignored)); } + /** + * Guicey bundles initialized. + * + * @param bundles actual bundles + * @param disabled disabled bundles + * @param ignored ignored bundles (duplicates) + */ public void bundlesInitialized(final List bundles, final List disabled, final List ignored) { @@ -115,33 +179,54 @@ public void bundlesInitialized(final List bundles, } } + /** + * Commands resolved. + * + * @param installed registered commands + */ public void commandsResolved(final List installed) { if (installed != null && !installed.isEmpty()) { broadcast(new CommandsResolvedEvent(context, installed)); } } + /** + * Installers resolved. + * + * @param installers actual installers + * @param disabled disabled installers + */ public void installersResolved(final List installers, final List> disabled) { broadcast(new InstallersResolvedEvent(context, installers, disabled)); } - public void manualExtensionsValidated(final List> extensions, final List> validated) { - if (!extensions.isEmpty()) { - broadcast(new ManualExtensionsValidatedEvent(context, extensions, validated)); - } - } - + /** + * Classpath scan done. + * + * @param extensions extensions detected + */ public void classpathExtensionsResolved(final List> extensions) { if (!extensions.isEmpty()) { broadcast(new ClasspathExtensionsResolvedEvent(context, extensions)); } } + /** + * Guice bundle initialization done. + */ public void initialized() { broadcast(new InitializedEvent(context)); } + /** + * Guice bundle run. + * + * @param configuration configuration + * @param configurationTree parsed configuration + * @param environment environment + */ + @SuppressWarnings("checkstyle:AnonInnerLength") public void runPhase(final Configuration configuration, final ConfigurationTree configurationTree, final Environment environment) { @@ -150,7 +235,12 @@ public void runPhase(final Configuration configuration, this.context.setEnvironment(environment); broadcast(new BeforeRunEvent(context)); // fire after complete initialization (final meta-event) - environment.lifecycle().addLifeCycleListener(new LifeCycle.Listener() { + environment.lifecycle().addEventListener(new LifeCycle.Listener() { + @Override + public void lifeCycleStarting(LifeCycle event) { + applicationStarting(); + } + @Override public void lifeCycleStarted(final LifeCycle event) { applicationStarted(); @@ -168,12 +258,37 @@ public void lifeCycleStopped(final LifeCycle event) { }); } + /** + * Guicey bundles run done. + * + * @param bundles started bundles + */ public void bundlesStarted(final List bundles) { if (!bundles.isEmpty()) { broadcast(new BundlesStartedEvent(context, bundles)); } } + /** + * Manual extensions validated. + * + * @param extensions all extensions + * @param validated manual extensions + */ + public void manualExtensionsValidated(final List> extensions, final List> validated) { + if (!extensions.isEmpty()) { + broadcast(new ManualExtensionsValidatedEvent(context, extensions, validated)); + } + } + + /** + * Guice modules analyzed. + * + * @param modules modules + * @param extensions resolved extensions + * @param transitiveModulesRemoved removed modules + * @param bindingsRemoved removed bindings + */ public void modulesAnalyzed(final List modules, final List> extensions, final List> transitiveModulesRemoved, @@ -181,10 +296,24 @@ public void modulesAnalyzed(final List modules, broadcast(new ModulesAnalyzedEvent(context, modules, extensions, transitiveModulesRemoved, bindingsRemoved)); } + /** + * Extensions resolved. + * + * @param extensions actual extensions + * @param disabled disabled extensions + */ public void extensionsResolved(final List> extensions, final List> disabled) { broadcast(new ExtensionsResolvedEvent(context, extensions, disabled)); } + /** + * Before injector creation. + * + * @param modules guice modules + * @param overriding overriding modules + * @param disabled disabled modules + * @param ignored ignored modules (duplicate) + */ public void injectorCreation(final List modules, final List overriding, final List disabled, @@ -192,10 +321,21 @@ public void injectorCreation(final List modules, broadcast(new InjectorCreationEvent(context, modules, overriding, disabled, ignored)); } + /** + * Injector available. + * + * @param injector injector + */ public void injectorPhase(final Injector injector) { this.context.setInjector(injector); } + /** + * Extensions installed. + * + * @param installer installer type + * @param installed extensions + */ public void extensionsInstalled(final Class installer, final List> installed) { if (installed != null && !installed.isEmpty()) { @@ -203,23 +343,47 @@ public void extensionsInstalled(final Class installe } } + /** + * All extensions installed. + * + * @param extensions installed extensions + */ public void extensionsInstalled(final List> extensions) { if (!extensions.isEmpty()) { broadcast(new ExtensionsInstalledEvent(context, extensions)); } } + /** + * Guice bundle started. Application run is up ahead. + */ public void applicationRun() { broadcast(new ApplicationRunEvent(context)); } + /** + * Application starting (application run method is called but neither managed nor jersey context not started). + */ + private void applicationStarting() { + broadcast(new ApplicationStartingEvent(context)); + } + /** + * Jersey configuration started. + * + * @param injectionManager injection manager + */ public void jerseyConfiguration(final InjectionManager injectionManager) { this.context.setInjectionManager(injectionManager); broadcast(new JerseyConfigurationEvent(context)); } - + /** + * Jersey extensions installed. + * + * @param installer installer type + * @param installed installed extensions + */ public void jerseyExtensionsInstalled(final Class installer, final List> installed) { if (installed != null && !installed.isEmpty()) { @@ -227,6 +391,11 @@ public void jerseyExtensionsInstalled(final Class in } } + /** + * All jersey extensions installed. + * + * @param extensions installed extensions + */ public void jerseyExtensionsInstalled(final List> extensions) { if (!extensions.isEmpty()) { broadcast(new JerseyExtensionsInstalledEvent(context, extensions)); @@ -241,16 +410,23 @@ public GuiceyLifecycle getStage() { } private void broadcast(final GuiceyLifecycleEvent event) { - listeners.forEach(l -> l.onEvent(event)); + if (!listeners.isEmpty()) { + final StatTimer timer = tracker.timer(Stat.ListenersTime); + final Stopwatch eventTimer = tracker.detailTimer(DetailStat.Listener, event.getClass()); + listeners.forEach(l -> l.onEvent(event)); + eventTimer.stop(); + timer.stop(); + } currentStage = event.getType(); } private void applicationStarted() { broadcast(new ApplicationStartedEvent(context)); + startupHook.run(); } private void applicationShutdown() { - broadcast(new ApplicationShotdownEvent(context)); + broadcast(new ApplicationShutdownEvent(context)); } private void applicationStopped() { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java similarity index 89% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java index b63841706..cf26374c1 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/BootstrapAwareModule.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.module.support; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; /** * Guice module, registered in bundle, may implement this to be able to use bootstrap object in module diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java index f5266e005..c37acc2a3 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationAwareModule.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.support; -import io.dropwizard.Configuration; +import io.dropwizard.core.Configuration; /** * Guice module, registered in bundle, may implement this to be able to use configuration object in module diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationTreeAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationTreeAwareModule.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationTreeAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/ConfigurationTreeAwareModule.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java similarity index 78% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java index a98dd22e8..1bd7f6e5f 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/DropwizardAwareModule.java @@ -1,16 +1,18 @@ package ru.vyarus.dropwizard.guice.module.support; import com.google.inject.AbstractModule; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; import ru.vyarus.dropwizard.guice.module.context.option.Options; import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder; import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -143,6 +145,41 @@ protected List configurations(final Class type) { return configurationTree().valuesByType(type); } + /** + * Search for exactly one annotated configuration value. It is not possible to provide the exact annotation + * instance, but you can create a class implementing annotation and use it for search. For example, guice + * {@link com.google.inject.name.Named} annotation has {@link com.google.inject.name.Names#named(String)}: + * it is important that real annotation instance and "pseudo" annotation object would be equal. + *

        + * For annotations without attributes use annotation type: {@link #annotatedConfiguration(Class)}. + *

        + * For multiple values use {@code configurationTree().annotatedValues()}. + * + * @param annotation annotation instance (equal object) to search for an annotated config path + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + */ + protected T annotatedConfiguration(final Annotation annotation) { + return configurationTree().annotatedValue(annotation); + } + + /** + * Search for exactly one configuration value with qualifier annotation (without attributes). For cases when + * annotation with attributes used - use {@link #annotatedConfiguration(java.lang.annotation.Annotation)} + * (current method would search only by annotation type, ignoring any (possible) attributes). + *

        + * For multiple values use {@code configurationTree().annotatedValues()}. + * + * @param qualifierType qualifier annotation type + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + */ + protected T annotatedConfiguration(final Class qualifierType) { + return configurationTree().annotatedValue(qualifierType); + } + /** * Raw configuration introspection info. Could be used for more sophisticated configuration searches then * provided in shortcut methods. @@ -192,7 +229,7 @@ protected Options options() { * Internally, state is linked to application instance, so it would be safe to use with concurrent tests. * Value could be accessed statically with application instance: * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#lookup( - * io.dropwizard.Application, Class)}. + * io.dropwizard.core.Application, Class)}. *

        * During application strartup, shared state could be requested with a static call * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#getStartupInstance()}, but only @@ -206,9 +243,10 @@ protected Options options() { * * @param key shared object key * @param value shared object + * @param shared object type * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState */ - public void shareState(final Class key, final Object value) { + public void shareState(final Class key, final V value) { SharedConfigurationState.getOrFail(environment(), STATE_NOT_FOUND).put(key, value); } @@ -225,7 +263,7 @@ public void shareState(final Class key, final Object value) { * @return shared object (possibly just created) * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState */ - public T sharedState(final Class key, final Supplier defaultValue) { + public T sharedState(final Class key, final Supplier defaultValue) { return SharedConfigurationState.getOrFail(environment(), STATE_NOT_FOUND).get(key, defaultValue); } @@ -239,7 +277,7 @@ public T sharedState(final Class key, final Supplier defaultValue) { * @return shared object * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState */ - protected Optional sharedState(final Class key) { + protected Optional sharedState(final Class key) { return SharedConfigurationState.lookup(environment(), key); } @@ -255,7 +293,19 @@ protected Optional sharedState(final Class key) { * @throws IllegalStateException if not value available * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState */ - protected T sharedStateOrFail(final Class key, final String message, final Object... args) { + protected T sharedStateOrFail(final Class key, final String message, final Object... args) { return SharedConfigurationState.lookupOrFail(environment(), key, message, args); } + + /** + * Reactive shared value access: if value already available action called immediately, otherwise action would + * be called when value set (note that value could be set only once). + * + * @param key shared object key + * @param action action to execute when value would be set + * @param value type + */ + protected void whenSharedStateReady(final Class key, final Consumer action) { + SharedConfigurationState.getOrFail(environment(), "Shared state not available").whenReady(key, action); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java similarity index 91% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java index 8df4e3bd4..9b00a06c1 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/EnvironmentAwareModule.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.module.support; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; /** * Guice module, registered in bundle, may implement this to be able to use environment object in module diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/OptionsAwareModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/OptionsAwareModule.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/OptionsAwareModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/OptionsAwareModule.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/support/scope/Prototype.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/scope/Prototype.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/support/scope/Prototype.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/support/scope/Prototype.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java index 30f38e6da..6c194bdbd 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigPath.java @@ -4,6 +4,7 @@ import ru.vyarus.java.generics.resolver.util.GenericsUtils; import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; @@ -44,7 +45,23 @@ public class ConfigPath { private final Object value; private final boolean customType; private final boolean objectDeclaration; + private final Annotation qualifier; + /** + * Create a parsed configuration path. + * + * @param root root path + * @param declarationClass configuration field declaration class + * @param declaredType field type + * @param valueType actual value type + * @param declaredTypeGenerics declared generics + * @param valueTypeGenerics value generics + * @param path full property path + * @param value value object + * @param customType object type (not primitive, enum or array) + * @param objectDeclaration raw Object declared + * @param qualifier field qualifier annotation (to use) + */ @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) public ConfigPath( final ConfigPath root, @@ -56,7 +73,8 @@ public ConfigPath( final String path, final Object value, final boolean customType, - final boolean objectDeclaration) { + final boolean objectDeclaration, + final Annotation qualifier) { this.declarationClass = declarationClass; this.declaredType = declaredType; this.valueType = valueType; @@ -67,6 +85,7 @@ public ConfigPath( this.customType = customType; this.objectDeclaration = objectDeclaration; this.root = root; + this.qualifier = qualifier; } /** @@ -188,6 +207,16 @@ public boolean isObjectDeclaration() { return objectDeclaration; } + /** + * Custom qualifiers might be used to simplify configuration bindings (because {@code @Config} qualifier requires + * the exact yaml path). + * + * @return custom qualifier annotation declared on config class or null + */ + public Annotation getQualifier() { + return qualifier; + } + /** * Useful for quick analysis when deeper generic type knowledge is not important (e.g. to know type in * list, set or map). @@ -209,7 +238,8 @@ public List getValueTypeGenericClasses() { } /** - * Useful to filter out core dropwizard properties (where root type would be {@link io.dropwizard.Configuration}. + * Useful to filter out core dropwizard properties (where root type would be + * {@link io.dropwizard.core.Configuration}. * * @return root configuration class where entire path started */ diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java index 946af4d76..869cc1bea 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigTreeBuilder.java @@ -9,15 +9,18 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.primitives.Primitives; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; +import com.google.inject.BindingAnnotation; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; import io.dropwizard.util.DataSize; import io.dropwizard.util.Duration; +import jakarta.inject.Qualifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.vyarus.java.generics.resolver.GenericsResolver; import ru.vyarus.java.generics.resolver.context.GenericsContext; +import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Type; import java.util.ArrayList; @@ -59,21 +62,21 @@ public final class ConfigTreeBuilder { /** * Packages to stop types introspection on (for sure non custom pojo types). */ - private static final ImmutableSet INTROSPECTION_STOP_PACKAGES = ImmutableSet.of( + private static final Set INTROSPECTION_STOP_PACKAGES = ImmutableSet.of( "java.", "groovy.", "com.google.common.collect", "sun." ); /** * Classes indicating final values (to stop introspection on). */ - private static final ImmutableSet INTROSPECTION_STOP_TYPES = ImmutableSet.of( + private static final Set INTROSPECTION_STOP_TYPES = ImmutableSet.of( Iterable.class, Optional.class, Duration.class, DataSize.class ); /** * Lower bounds for value types declarations (to use instead of actual implementation for constant declaration). */ - private static final ImmutableSet COMMON_VALUE_TYPES = ImmutableSet.of( + private static final Set COMMON_VALUE_TYPES = ImmutableSet.of( List.class, Set.class, Map.class, Multimap.class ); @@ -158,7 +161,7 @@ private static List resolveRootTypes(final List roots, final Class * @param object analyzed part instance (may be null) * @return all configuration paths values */ - @SuppressWarnings({"checkstyle:CyclomaticComplexity", "PMD.AvoidLiteralsInIfCondition"}) + @SuppressWarnings("checkstyle:CyclomaticComplexity") private static List resolvePaths(final SerializationConfig config, final ConfigPath root, final List content, @@ -290,7 +293,8 @@ private static ConfigPath createItem(final ConfigPath root, fullPath(root, prop), value, customType, - objectDeclared); + objectDeclared, + findQualifier(prop)); } /** @@ -461,7 +465,7 @@ private static Object readValue(final AnnotatedMember member, final Object objec final Object res; final AccessibleObject accessor = (AccessibleObject) member.getMember(); // case: private field - if (!accessor.isAccessible()) { + if (!accessor.canAccess(object)) { accessor.setAccessible(true); try { res = member.getValue(object); @@ -478,4 +482,30 @@ private static Object readValue(final AnnotatedMember member, final Object objec private static String fullPath(final ConfigPath root, final BeanPropertyDefinition prop) { return (root == null ? "" : root.getPath() + ".") + prop.getName(); } + + private static Annotation findQualifier(final BeanPropertyDefinition prop) { + // field in priority + Annotation ann = null; + if (prop.getField() != null) { + ann = findQualifierAnnotation(prop.getField().getAllAnnotations().annotations()); + } + // check getter + if (ann == null && prop.getGetter() != null) { + ann = findQualifierAnnotation(prop.getGetter().getAllAnnotations().annotations()); + } + + return ann; + } + + private static Annotation findQualifierAnnotation(final Iterable anns) { + for (Annotation ann : anns) { + for (Annotation marker : ann.annotationType().getAnnotations()) { + final Class type = marker.annotationType(); + if (type.equals(Qualifier.class) || type.equals(BindingAnnotation.class)) { + return ann; + } + } + } + return null; + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java similarity index 57% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java index bd5b688eb..cedcb791e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/ConfigurationTree.java @@ -1,8 +1,11 @@ package ru.vyarus.dropwizard.guice.module.yaml; -import io.dropwizard.Configuration; +import com.google.common.base.Preconditions; +import io.dropwizard.core.Configuration; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; import ru.vyarus.dropwizard.guice.module.support.ConfigurationTreeAwareModule; +import java.lang.annotation.Annotation; import java.util.*; import java.util.stream.Collectors; @@ -27,6 +30,11 @@ * Also, object is accessible inside guicey bundles * {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment#configurationTree()} and guice modules: * {@link ConfigurationTreeAwareModule}. + *

        + * Qualified configuration paths also detected: fields or getters annotated with qualifier annotation. + * Qualifier annotation is an annotation annotated with {@link com.google.inject.BindingAnnotation} + * (e.g. {@link com.google.inject.name.Named}) or {@link jakarta.inject.Qualifier} + * (e.g. {@link jakarta.inject.Named}). * * @author Vyacheslav Rusakov * @see ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule @@ -44,10 +52,22 @@ public class ConfigurationTree { // unique custom types from paths (could be bound by type - no duplicates) private final List uniqueTypePaths; + /** + * Create a configuration tree. + * + * @param rootTypes root properties + */ public ConfigurationTree(final List rootTypes) { this(rootTypes, Collections.emptyList(), Collections.emptyList()); } + /** + * Create a configuration tree. + * + * @param rootTypes root properties + * @param paths configuration paths + * @param uniqueTypePaths unique object paths + */ public ConfigurationTree(final List rootTypes, final List paths, final List uniqueTypePaths) { @@ -59,7 +79,8 @@ public ConfigurationTree(final List rootTypes, } /** - * @return configuration hierarchy classes (including {@link io.dropwizard.Configuration}) and custom interfaces + * @return configuration hierarchy classes (including {@link io.dropwizard.core.Configuration}) and custom + * interfaces */ public List getRootTypes() { return new ArrayList<>(rootTypes); @@ -98,7 +119,7 @@ public List getUniqueTypePaths() { // ---------------------------------------------------------- Structure search (tree traverse examples) /** - * Case insensitive exact match. + * Case-insensitive exact match. * * @param path path to find descriptor for * @return path descriptor or null if not found @@ -110,6 +131,80 @@ public ConfigPath findByPath(final String path) { .orElse(null); } + /** + * Search configuration paths annotated by qualifier annotation. It is not possible to provide the exact annotation + * instance, but you can create a class implementing annotation and use it for search. For example, guice + * {@link com.google.inject.name.Named} annotation has {@link com.google.inject.name.Names#named(String)}: + * it is important that real annotation instance and "pseudo" annotation object would be equal (by equals). + *

        + * For annotations without attributes use annotation type: {@link #findAllByAnnotation(Class)}. + * + * @param annotation annotation instance (equal object) to search for annotated config paths + * @return list of annotated (on field or getter) configuration paths + */ + public List findAllByAnnotation(final Annotation annotation) { + return paths.stream() + .filter(path -> path.getQualifier() != null && annotation.equals(path.getQualifier())) + .collect(Collectors.toList()); + } + + /** + * Search configuration paths annotated by qualifier annotation (without attributes). For cases when annotation + * with attributes used - use {@link #findAllByAnnotation(java.lang.annotation.Annotation)} (current method would + * also work for all annotations because it compares by type only, but it might be not what was searched for). + * + * @param qualifierType qualifier annotation type + * @return list of annotated (on field or getter) configuration paths + */ + public List findAllByAnnotation(final Class qualifierType) { + return paths.stream() + .filter(path -> path.getQualifier() != null + && qualifierType.equals(path.getQualifier().annotationType())) + .collect(Collectors.toList()); + } + + /** + * Search for exactly one configuration path annotated with qualifier annotation. It is not possible to provide + * the exact annotation instance, but you can create a class implementing annotation and use it for search. + * For example, guice {@link com.google.inject.name.Named} annotation has + * {@link com.google.inject.name.Names#named(String)}: it is important that real annotation instance and "pseudo" + * annotation object would be equal (by equals). + *

        + * For annotations without attributes use annotation type: {@link #findByAnnotation(Class)}. + * + * @param annotation annotation instance (equal object) to search for an annotated config path + * @return found configuration path or null + * @throws java.lang.IllegalStateException if multiple paths found + */ + public ConfigPath findByAnnotation(final Annotation annotation) { + final List res = findAllByAnnotation(annotation); + Preconditions.checkState(res.size() <= 1, + "Multiple configuration paths qualified with annotation %s:\n%s", + RenderUtils.renderAnnotation(annotation), res.stream() + .map(path -> "\t\t" + path.toString()) + .collect(Collectors.joining("\n"))); + return res.isEmpty() ? null : res.get(0); + } + + /** + * Search for exactly one configuration path annotated with qualified annotation. For cases when annotation with + * attributes used - use {@link #findByAnnotation(java.lang.annotation.Annotation)} (current method would + * search only by annotation type, ignoring any (possible) attributes). + * + * @param qualifierType qualifier annotation type + * @return found configuration path or null + * @throws java.lang.IllegalStateException if multiple paths found + */ + public ConfigPath findByAnnotation(final Class qualifierType) { + final List res = findAllByAnnotation(qualifierType); + Preconditions.checkState(res.size() <= 1, + "Multiple configuration paths qualified with annotation type @%s:\n%s", + qualifierType.getSimpleName(), res.stream() + .map(path -> "\t\t" + path.toString()) + .collect(Collectors.joining("\n"))); + return res.isEmpty() ? null : res.get(0); + } + /** * Useful for searching multiple custom types. *

        {@code class Config {
        @@ -276,6 +371,82 @@ public  K valueByUniqueDeclaredType(final Class type) {
                         .orElse(null);
             }
         
        +    /**
        +     * Search configuration values by qualifier annotation. It is not possible to provide the exact annotation instance,
        +     * but you can create a class implementing annotation and use it for search. For example, guice
        +     * {@link com.google.inject.name.Named} annotation has {@link com.google.inject.name.Names#named(String)}:
        +     * it is important that real annotation instance and "pseudo" annotation object would be equal (by equals).
        +     * 

        + * For annotations without attributes use annotation type: {@link #annotatedValue(Class)}. + * + * @param annotation annotation instance (equal object) to search for annotated config paths + * @param target value type + * @return all non-null annotated configuration values + * @see #findAllByAnnotation(java.lang.annotation.Annotation) + */ + @SuppressWarnings("unchecked") + public Set annotatedValues(final Annotation annotation) { + return findAllByAnnotation(annotation).stream() + .map(path -> (T) path.getValue()) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + /** + * Search configuration values by qualifier annotation (without attributes). For cases when annotation with + * attributes used - use {@link #annotatedValues(java.lang.annotation.Annotation)} (current method would + * also work for all annotations because it compares by type only, but it might be not what was searched for). + * + * @param qualifierType qualifier annotation type + * @param target value type + * @return all non-null annotated configuration values + * @see #findAllByAnnotation(Class) + */ + @SuppressWarnings("unchecked") + public Set annotatedValues(final Class qualifierType) { + return findAllByAnnotation(qualifierType).stream() + .map(path -> (T) path.getValue()) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + /** + * Search for exactly one qualified configuration value. It is not possible to provide the exact annotation + * instance, but you can create a class implementing annotation and use it for search. For example, guice + * {@link com.google.inject.name.Named} annotation has {@link com.google.inject.name.Names#named(String)}: + * it is important that real annotation instance and "pseudo" annotation object would be equal (by equals). + *

        + * For annotations without attributes use annotation type: {@link #annotatedValue(Class)}. + * + * @param annotation annotation instance (equal object) to search for an annotated config path + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + * @see #findByAnnotation(java.lang.annotation.Annotation) + */ + @SuppressWarnings("unchecked") + public T annotatedValue(final Annotation annotation) { + final ConfigPath path = findByAnnotation(annotation); + return path == null ? null : (T) path.getValue(); + } + + /** + * Search for exactly one configuration value with qualifier annotation (without attributes). For cases when + * annotation with attributes used - use {@link #findByAnnotation(java.lang.annotation.Annotation)} (current + * method would search only by annotation type, ignoring any (possible) attributes). + * + * @param qualifierType qualifier annotation type + * @param value type + * @return qualified configuration value or null + * @throws java.lang.IllegalStateException if multiple values found + * @see #findByAnnotation(Class) + */ + @SuppressWarnings("unchecked") + public T annotatedValue(final Class qualifierType) { + final ConfigPath path = findByAnnotation(qualifierType); + return path == null ? null : (T) path.getValue(); + } + private void sortContent() { final Comparator comparator = (o1, o2) -> { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/Config.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/Config.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/Config.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/Config.java diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java similarity index 68% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java index 15ae5267f..7f8254de6 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigBindingModule.java @@ -1,12 +1,19 @@ package ru.vyarus.dropwizard.guice.module.yaml.bind; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; import com.google.inject.AbstractModule; import com.google.inject.Key; import com.google.inject.binder.LinkedBindingBuilder; import com.google.inject.util.Providers; -import io.dropwizard.Configuration; -import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; +import io.dropwizard.core.Configuration; import ru.vyarus.dropwizard.guice.module.yaml.ConfigPath; +import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree; +import ru.vyarus.java.generics.resolver.context.container.ParameterizedTypeImpl; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; /** * Binds configuration constants. Bindings are qualified with {@link Config}. @@ -26,15 +33,21 @@ * {@link ConfigurationTree} instance is also bound directly to be used for custom configuration analysis. * * @author Vyacheslav Rusakov - * @since 04.05.2018 * @see Config for more info on usage * @see ru.vyarus.dropwizard.guice.GuiceyOptions#BindConfigurationByPath + * @since 04.05.2018 */ public class ConfigBindingModule extends AbstractModule { private final Configuration configuration; private final ConfigurationTree tree; + /** + * Create configuration bindings module. + * + * @param configuration configuration instance. + * @param tree parsed configuration + */ public ConfigBindingModule(final Configuration configuration, final ConfigurationTree tree) { this.configuration = configuration; this.tree = tree; @@ -44,11 +57,43 @@ public ConfigBindingModule(final Configuration configuration, final Configuratio protected void configure() { bind(ConfigurationTree.class).toInstance(tree); + bindCustomQualifiers(); bindRootTypes(); bindUniqueSubConfigurations(); bindValuePaths(); } + /** + * Bind configuration properties, annotated with custom qualifiers. If the same "qualifier + type" is detected, + * all such values are grouped with {@code Set}. + */ + private void bindCustomQualifiers() { + final Multimap, ConfigPath> bindings = LinkedHashMultimap.create(); + for (ConfigPath item : tree.getPaths()) { + if (item.getQualifier() != null) { + final Key key = Key.get(item.getDeclaredTypeWithGenerics(), item.getQualifier()); + bindings.put(key, item); + } + } + + for (Key key : bindings.keySet()) { + final Collection values = bindings.get(key); + // single value case + final ConfigPath first = values.iterator().next(); + Object value = first.getValue(); + + Key bindingKey = key; + if (values.size() > 1) { + // aggregate multiple values into set + // NOTE no need to check types compatibility because matching was based on pre-computed keys + value = values.stream().map(ConfigPath::getValue).collect(Collectors.toSet()); + bindingKey = Key.get(new ParameterizedTypeImpl(Set.class, first.getDeclaredTypeWithGenerics()), + first.getQualifier()); + } + bindValue(bind(bindingKey), value); + } + } + /** * Bind configuration hierarchy: all superclasses and direct interfaces for each level (except common interfaces). diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java similarity index 92% rename from src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java index 56008b38f..fc79f9017 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/module/yaml/bind/ConfigImpl.java @@ -20,8 +20,16 @@ public class ConfigImpl implements Config, Serializable { private static final long serialVersionUID = 0; + /** + * Value. + */ private final String val; + /** + * Create configuration qualifier. + * + * @param val qualifier value + */ public ConfigImpl(final String val) { this.val = checkNotNull(val, "name"); } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java new file mode 100644 index 000000000..a0ced7888 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java @@ -0,0 +1,479 @@ +package ru.vyarus.dropwizard.guice.test; + +import com.google.common.base.Preconditions; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.UriBuilder; +import org.glassfish.jersey.client.JerseyClient; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; +import ru.vyarus.dropwizard.guice.url.AppUrlBuilder; + +/** + * {@link JerseyClient} support for direct web tests (complete dropwizard startup). + *

        + * Client support maintains single {@link JerseyClient} instance. It may be used for calling any urls (not just + * application). Class provides many utility methods for automatic construction of base context paths, so + * tests could be completely independent of actual configuration. + *

        + * Client customization is possible through custom + * {@link ru.vyarus.dropwizard.guice.test.client.TestClientFactory} implementation. + *

        + * Dropwizard configurations for rest, app and admin contexts: + *

          + *
        • {@code server.applicationContextPath} - app context path ("/" by default, "/application" for simple + * server)
        • + *
        • {@code server.adminContextPath} - admin context path ("/" by default, "/admin" for simple server)
        • + *
        • {@code server.rootPath} - rest context path ("/" by default)
        • + *
        + * The rest prefix is {@code server.applicationContextPath} + {@code server.rootPath}. + *

        + * The class provides access for 4 clients (with the same api + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient}): + *

          + *
        • {@link ru.vyarus.dropwizard.guice.test.ClientSupport} itself is a client for application root
        • + *
        • {@link #restClient()} specialized rest client (mapped rest path counted)
        • + *
        • {@link #appClient()} client for the app context (context mapping path counted)
        • + *
        • {@link #adminClient()} client for the admin context (context mapping path and port counted)
        • + *
        + * The main idea of clients is the ability to create a client for any base path to shorten urls in tests. + * Also, each client could declare its own defaults, applied to all requests (for example, useful for authorization). + *

        + * There is also a specialized {@link #externalClient(String, Object...)} for custom clients for creating remote api + * clients (with the same client api): + * {@code support.externalClient("http://localhost:8080/some/path").get("/some/resource")}. + *

        + * There is a special class-based client constructor {@link #restClient(Class)} for resources. This makes tests + * type-safe as the target resource path is obtained directly from the class annotation. Also, such clients + * could use resource class method calls for request configuration + * ({@link ru.vyarus.dropwizard.guice.test.client.ResourceClient#method(ru.vyarus.dropwizard.guice.url.util.Caller)}). + *

        + * Note: defaults, declared on root {@link ru.vyarus.dropwizard.guice.test.ClientSupport} class would be inherited + * by sub clients (excluding external clients). + *

        + * The root client is not much useful, so the assumed usage is to get the required client and use it for actual calls. + *

        + * Class also provides base urls and related jersey targets (pure jersey api): + *

          + *
        • {@link #basePathRoot()} and {@link #target(String, Object...)}
        • + *
        • {@link #basePathRest()} and {@link #targetRest(String, Object...)}
        • + *
        • {@link #basePathApp()} and {@link #targetApp(String, Object...)}
        • + *
        • {@link #basePathAdmin()} and {@link #targetAdmin(String, Object...)}
        • + *
        + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.test.client.TestClient + * @since 04.05.2020 + */ +public class ClientSupport extends TestClient implements AutoCloseable { + + private final DropwizardTestSupport support; + private final AppUrlBuilder urlBuilder; + private final TestClientFactory factory; + private JerseyClient client; + + /** + * Create a client. + * + * @param support dropwizard test support + */ + public ClientSupport(final DropwizardTestSupport support) { + this(support, null); + } + + /** + * Create client with custom factory. + * + * @param support dropwizard test support + * @param factory custom client factory + */ + public ClientSupport(final DropwizardTestSupport support, + final @Nullable TestClientFactory factory) { + this(support, factory, null); + } + + /** + * Create client with custom factory. + * + * @param support dropwizard test support + * @param factory custom client factory + * @param defaults defaults for all clients (could be overridden by the client-specific defaults) + */ + public ClientSupport(final DropwizardTestSupport support, + final @Nullable TestClientFactory factory, + final @Nullable TestRequestConfig defaults) { + super(defaults); + this.support = support; + this.urlBuilder = new AppUrlBuilder(support::getEnvironment); + this.factory = factory == null ? new DefaultTestClientFactory() : factory; + } + + /** + * Single client instance maintained within test and method will always return the same instance. + * + * @return client instance + */ + public JerseyClient getClient() { + synchronized (this) { + if (client == null) { + client = factory.create(support); + } + return client; + } + } + + /** + * Shortcut to be able to quickly build an apache-connector-based client. The default urlconnector-based + * client is better for multipart calls (apache client + * has problems). + * But the apache client is better for PATCH calls because the default urlconnector will complain on java > 16. + *

        + * With this shortcut it would be possible to use both clients in one test. + *

        + * Applied defaults are inherited. + * + * @return client based on apache connector + */ + public ClientSupport apacheClient() { + return factory instanceof ApacheTestClientFactory ? this + : new ClientSupport(support, new ApacheTestClientFactory(), defaults); + } + + /** + * Shortcut to be able to quickly build an apache-connector-based client. The default urlconnector-based + * client is better for multipart calls (apache client + * has problems). + * But the apache client is better for PATCH calls because the default urlconnector will complain on java > 16. + *

        + * With this shortcut it would be possible to use both clients in one test. + *

        + * Applied defaults are inherited. + * + * @return client based on apache connector + */ + public ClientSupport urlconnectorClient() { + return factory instanceof DefaultTestClientFactory ? this + : new ClientSupport(support, new DefaultTestClientFactory(), defaults); + } + + // -------------------------------------------------------------------------- SERVER PATHS + + /** + * @return app context port + * @throws NullPointerException for guicey test (when web not started) + */ + public int getPort() { + return support.getLocalPort(); + } + + /** + * @return admin context port + * @throws NullPointerException for guicey test (when web not started) + */ + public int getAdminPort() { + return support.getAdminPort(); + } + + /** + * Root application path is, usually, not very interesting so consider using + * {@link #basePathApp()}, {@link #basePathRest()} or {@link #basePathAdmin()} instead. + * + * @return root application path (localhost + port) + * @throws NullPointerException for guicey test (when web not started) + */ + public String basePathRoot() { + return urlBuilder.root("/"); + } + + /** + * @return base path for application context + * @deprecated use {@link #basePathApp()} + */ + @Deprecated + public String basePathMain() { + return basePathApp(); + } + + /** + * For example, with the default configuration it would be "http://localhost:8080/". If + * "server.applicationContextPath" is changed to "/someth", then the method will return + * "http://localhost:8080/someth/". + *

        + * Returned path will always end with a slash. + * + * @return base path for app context + * @throws NullPointerException for guicey test (when web not started) + * @see #targetApp(String, Object...) + * @see #appClient() + */ + public String basePathApp() { + return urlBuilder.app("/"); + } + + /** + * For example, with the default configuration it would be "http://localhost:8081/". For the "simple" server, it + * would be "http://localhost:8080/adminPath/". If "server.adminContextPath" is changed to "/adm", then the method + * will return "http://localhost:8081/adm/" for the default server and "http://localhost:8080/adm/" for "simple" + * server. + *

        + * Returned path will always end with a slash. + * + * @return base path for admin context + * @throws NullPointerException for guicey test (when web not started) + * @see #targetAdmin(String, Object...) + * @see #adminClient() + */ + public String basePathAdmin() { + return urlBuilder.admin("/"); + } + + /** + * For example, with the default configuration it would be "http://localhost:8080/". If "server.rootPath" + * is changed to "/api", then the method will return "http://localhost:8080/api/". + * If the app context mapping changed from root, then the returned path will count it too + * (e.g. "http://localhost:8080/root/rest/", when "server.applicationContextPath" is "/root"). + *

        + * Returned path will always end with a slash. + * + * @return base path for rest + * @throws NullPointerException for guicey test (when web not started) + * @see #targetRest(String, Object...) + * @see #restClient() + * @see #restClient(Class) + */ + public String basePathRest() { + return urlBuilder.rest("/"); + } + + // ----------------------------------------------------------------------- PURE JERSEY API + + /** + * Create a jersey {@link WebTarget} for a path, relative to application root (e.g. + * "http://localhost:8080/" + provided path)". + *

        + * It could be also used to request any external api, for example: + * {@code target("http://somewhere.com/api/smth/").request().buildGet().invoke()}. + *

        + * String format could be used to format the path: + * {@code .target("/smth/%s/other", 12).request().buildGet().invoke()} + *

        + * NOTE: could be used with guicey-only tests (when web part not started) to call any external url. + *

        + * IMPORTANT: it would not apply configured defaults (like {@link #defaultHeader(String, Object)}). + * Use builders ({@link #build(String, String, Object, Object...)}) or shortcuts + * {@link #get(String, Class, Object...)} to cal with defaults. + * + * @param path target path, relative to client root or absolute external path (could contain String.format() + * placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object + * @see #basePathRoot() + * @see #externalClient(String, Object...) for external api client + */ + @Override + public WebTarget target(final String path, final Object... args) { + if (isHttp(path)) { + return getClient().target(String.format(path, args)); + } + return super.target(path, args); + } + + /** + * Shortcut for {@link WebTarget} creation for the application context path. + * + * @param path target path, relative to the admin context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object for the admin context + * @deprecated use {@link #targetApp(String, Object...)} instead + */ + @Deprecated + public WebTarget targetMain(final String path, final Object... args) { + return targetApp(path, args); + } + + /** + * Shortcut for {@link WebTarget} creation for the app context path. + *

        + * Example: {@code .targetApp("/path").request().buildGet().invoke()} would call "http://localhost:8080/path" + * (or, if root context mapping changed with {@code server.applicationContextPath = "root"}, + * "http://localhost:8080/root/path"). + *

        + * String format could be used to format the path: + * {@code .targetApp("/smth/%s/other", 12).request().buildGet().invoke()} + *

        + * IMPORTANT: it would not apply configured defaults (like {@link #defaultHeader(String, Object)}). + * Use app client {@link #appClient()} to call with defaults. + * + * @param path target path, relative to the app context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object for the app context + * @throws NullPointerException for guicey test (when web not started) + * @see #basePathApp() for base use construction details + * @see #appClient() + */ + public WebTarget targetApp(final String path, final Object... args) { + return target(urlBuilder.app(path, args)); + } + + /** + * Shortcut for {@link WebTarget} creation for the admin context path. + *

        + * Example: {@code .targetAdmin("/path").request().buildGet().invoke()} would call "http://localhost:8081/path" + * (or, if admin context mapping changed with {@code server.adminContextPath = "admin"}, + * "http://localhost:8081/admin/path"). + *

        + * String format could be used to format the path: + * {@code .targetAdmin("/smth/%s/other", 12).request().buildGet().invoke()} + *

        + * IMPORTANT: it would not apply configured defaults (like {@link #defaultHeader(String, Object)}). + * Use admin client {@link #adminClient()} to call with defaults. + * + * @param path target path, relative to the admin context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object for the admin context + * @throws NullPointerException for guicey test (when web not started) + * @see #basePathAdmin() for base use construction details + * @see #adminClient() + */ + public WebTarget targetAdmin(final String path, final Object... args) { + return target(urlBuilder.admin(path, args)); + } + + /** + * Shortcut for {@link WebTarget} creation for the rest context path. + *

        + * Example: {@code .targetRest("/path").request().buildGet().invoke()} would call "http://localhost:8080/path" + * (or, if rest context mapping changed with {@code server.rootPath = "api"}, + * "http://localhost:8081/api/path"). + *

        + * String format could be used to format the path: + * {@code .targetRest("/smth/%s/other", 12).request().buildGet().invoke()} + *

        + * IMPORTANT: it would not apply configured defaults (like {@link #defaultHeader(String, Object)}). + * Use rest client {@link #restClient()} (or specialized {@link #restClient(Class)}) to call with defaults. + * + * @param path target path, relative to the rest context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object for rest context + * @throws NullPointerException for guicey test (when web not started) + * @see #basePathRest() for base use construction details + * @see #restClient() + */ + public WebTarget targetRest(final String path, final Object... args) { + return target(urlBuilder.rest(path, args)); + } + + // ------------------------------------------------------------------------------ CLIENTS + + /** + * Construct a rest client. Client would be configured with the current rest path root + * (@link {@link #basePathRest()}), so requests would need to use only relative paths. + *

        + * Typed rest client could be obtained from this generic client: {@code restClient().subClient(Resource.class)}; + *

        + * Inherits current defaults (like {@link #defaultHeader(String, Object)}. + * + * @return rest client + */ + public TestClient restClient() { + // client INHERITS support defaults + return new TestClient<>(() -> getClient().target(basePathRest()), defaults); + } + + /** + * Construct a specialized rest client for the given resource class. Client would be configured with the current + * rest path root (@link {@link #basePathRest()}) together with the resource class path (obtained from annotation), + * so requests would need to use paths, relative to resource. + *

        + * Also, such client provide methods to configure requests direct from resource methods + * {@link ResourceClient#method(ru.vyarus.dropwizard.guice.url.util.Caller)}. + *

        + * Inherits current defaults (like {@link #defaultHeader(String, Object)}. + * + * @param resource resource class to configure target url from + * @param resource type + * @return rest client for the given resource class + */ + @Override + public ResourceClient restClient(final Class resource) { + final String target = UriBuilder.newInstance().path(resource).toTemplate(); + // client INHERITS support defaults + return new ResourceClient<>(() -> getClient().target(basePathRest()).path(target), defaults, resource); + } + + /** + * Construct a client for the app context. Client would be configured with the current app context path root + * (@link {@link #basePathApp()}), so requests would need to use only relative paths. + *

        + * Note: by default, root client {@link ru.vyarus.dropwizard.guice.test.ClientSupport} itself and + * app context client would be the same, because usually app context is "/". But, you can still prefer + * app client to shield from potential app context path changes. + *

        + * Inherits current defaults (like {@link #defaultHeader(String, Object)}. + * + * @return app client + */ + public TestClient appClient() { + // client INHERITS support defaults + return new TestClient<>(() -> getClient().target(basePathApp()), defaults); + } + + /** + * Construct a client for the admin context. Client would be configured with the current admin context path root + * (@link {@link #basePathAdmin()}), so requests would need to use only relative paths. + *

        + * Inherits current defaults (like {@link #defaultHeader(String, Object)}. + * + * @return admin client + */ + public TestClient adminClient() { + // client INHERITS support defaults + return new TestClient<>(() -> getClient().target(basePathAdmin()), defaults); + } + + /** + * Construct a client for external url. + *

        + * Example of variables usage: {@code externalClient("http://localhost:8080/%s/other", 12)}. + *

        + * Will not inherit current defaults. + * + * @param url external url, started with "http(s)" (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return client for the given external url + */ + public TestClient externalClient(final String url, final Object... args) { + checkHttp(url); + // custom external client - no defaults inherited + return new TestClient<>(() -> getClient().target(String.format(url, args)), null); + } + + @Override + public void close() throws Exception { + synchronized (this) { + if (client != null) { + client.close(); + client = null; + } + } + } + + @Override + protected WebTarget getRoot() { + return getClient().target(basePathRoot()); + } + + private boolean isHttp(final String path) { + return path.toLowerCase().startsWith("http"); + } + + private void checkHttp(final String host) { + Preconditions.checkState(isHttp(host), + "Host must include target server protocol and the host name (like http://myhost.com)"); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/EnableHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/EnableHook.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/test/EnableHook.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/EnableHook.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java new file mode 100644 index 000000000..2297c29c9 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java @@ -0,0 +1,224 @@ +package ru.vyarus.dropwizard.guice.test; + +import com.google.common.base.Preconditions; +import com.google.inject.Key; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.Command; +import io.dropwizard.configuration.ConfigurationSourceProvider; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; + +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + + +/** + * An alternative to {@link DropwizardTestSupport} which does not run jetty (web part) allowing to test only guice + * context. Internally, {@link TestCommand} used instead of {@link io.dropwizard.core.cli.ServerCommand}. + *

        + * Supposed to be used in cases when application startup fail must be tested: + * {@code new GuiceyTestSupport(MyApp.class, (String) null).before()}. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 03.02.2022 + */ +public class GuiceyTestSupport extends DropwizardTestSupport { + + /** + * Create test support. + * + * @param applicationClass application class + * @param configPath configuration file path + * @param configOverrides configuration overrides + */ + public GuiceyTestSupport(final Class> applicationClass, + final @Nullable String configPath, + final ConfigOverride... configOverrides) { + this(applicationClass, configPath, (String) null, configOverrides); + } + + /** + * Create test support. + * + * @param applicationClass application class + * @param configPath configuration file path + * @param configSourceProvider configuration source provider (optional) + * @param configOverrides configuration overrides + */ + public GuiceyTestSupport(final Class> applicationClass, + final @Nullable String configPath, + final @Nullable ConfigurationSourceProvider configSourceProvider, + final ConfigOverride... configOverrides) { + this(applicationClass, configPath, configSourceProvider, null, configOverrides); + } + + /** + * Create test support. + * + * @param applicationClass application class + * @param configPath configuration file path + * @param configSourceProvider configuration source provider + * @param customPropertyPrefix configuration overrides prefix + * @param configOverrides configuration overrides + */ + public GuiceyTestSupport(final Class> applicationClass, + final @Nullable String configPath, + final @Nullable ConfigurationSourceProvider configSourceProvider, + final @Nullable String customPropertyPrefix, + final ConfigOverride... configOverrides) { + super(applicationClass, configPath, configSourceProvider, customPropertyPrefix, + new CmdProvider<>(), configOverrides); + } + + /** + * Create test support. + * + * @param applicationClass application class + * @param configPath configuration file path + * @param customPropertyPrefix configuration overrides prefix + * @param configOverrides configuration overrides + */ + public GuiceyTestSupport(final Class> applicationClass, + final @Nullable String configPath, + final @Nullable String customPropertyPrefix, + final ConfigOverride... configOverrides) { + super(applicationClass, configPath, customPropertyPrefix, new CmdProvider<>(), configOverrides); + } + + /** + * Create test support. + * + * @param applicationClass application class + * @param configuration manual configuration instance + */ + public GuiceyTestSupport(final Class> applicationClass, + final C configuration) { + super(applicationClass, configuration, new CmdProvider<>()); + } + + /** + * By default, guicey simulates jetty lifecycle to support for {@link io.dropwizard.lifecycle.Managed} and + * {@link org.eclipse.jetty.util.component.LifeCycle} objects. + *

        + * It might be required in test to avoid starting managed objects (especially all managed in application) because + * important (for test) services replaced with mocks (and no need to wait for the rest of the application). + * + * @return test support instance for chained calls + */ + public GuiceyTestSupport disableManagedLifecycle() { + ((CmdProvider) this.commandInstantiator).disableManagedSimulation(); + return this; + } + + /** + * Register configuration modifiers. + * + * @param modifiers configuration modifiers + * @return support object instance for chained calls + */ + @SafeVarargs + public final GuiceyTestSupport configModifiers(final ConfigModifier... modifiers) { + return configModifiers(Arrays.asList(modifiers)); + } + + /** + * Register configuration modifiers. + * + * @param modifiers configuration modifiers + * @return support object instance for chained calls + */ + public GuiceyTestSupport configModifiers(final List> modifiers) { + ((CmdProvider) this.commandInstantiator).configModifiers(modifiers); + return this; + } + + /** + * Normally, {@link #before()} and {@link #after()} methods are called separately. This method is a shortcut + * mostly for errors testing when {@link #before()} assumed to fail to make sure {@link #after()} will be called + * in any case: {@code testSupport.run(null)}. + * + * @param callback callback (may be null) + * @param result type + * @return callback result + * @throws Exception any appeared exception + */ + public T run(final @Nullable TestSupport.RunCallback callback) throws Exception { + return TestSupport.run(this, callback); + } + + /** + * Normally, {@link #before()} and {@link #after()} methods are called separately. This method is a shortcut + * mostly for errors testing when {@link #before()} assumed to fail to make sure {@link #after()} will be called + * in any case: {@code testSupport.run(null)}. + * + * @return execution result (with all required objects for verification) + * @throws Exception any appeared exception + */ + public RunResult run() throws Exception { + return TestSupport.run(this); + } + + /** + * Shortcut for accessing guice beans. + * + * @param type target bean type + * @param bean type + * @return bean instance + */ + public T getBean(final Class type) { + return TestSupport.getBean(this, type); + } + + /** + * Shortcut for accessing guice beans. + * + * @param key binding key + * @param bean type + * @return bean instance + */ + public T getBean(final Key key) { + return TestSupport.getBean(this, key); + } + + @Override + public void after() { + super.after(); + final TestCommand cmd = ((CmdProvider) commandInstantiator).command; + if (cmd != null) { + cmd.stop(); + } + } + + @SuppressWarnings("checkstyle:VisibilityModifier") + static class CmdProvider implements Function, Command> { + + public TestCommand command; + private boolean simulateManaged = true; + private final List> modifiers = new ArrayList<>(); + + public void disableManagedSimulation() { + Preconditions.checkState(command == null, "Command already initialized"); + simulateManaged = false; + } + + public void configModifiers(final List> modifiers) { + Preconditions.checkState(command == null, "Command already initialized"); + this.modifiers.addAll(modifiers); + } + + @Override + public Command apply(final Application application) { + Preconditions.checkState(command == null, "Command already created"); + command = new TestCommand<>(application, simulateManaged, modifiers); + return command; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java new file mode 100644 index 000000000..72d8a0fec --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java @@ -0,0 +1,127 @@ +package ru.vyarus.dropwizard.guice.test; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.EnvironmentCommand; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; + +import java.util.List; + +/** + * Lightweight variation of server command for testing purposes. + * Handles managed objects lifecycle. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 23.10.2014 + */ +public class TestCommand extends EnvironmentCommand { + + private final Logger logger = LoggerFactory.getLogger(TestCommand.class); + private final Class configurationClass; + private final boolean simulateManaged; + private final List> modifiers; + private ContainerLifeCycle container; + + /** + * Create command. + * + * @param application application instance + */ + public TestCommand(final Application application) { + this(application, true); + } + + /** + * Create command. + * + * @param application application instance + * @param simulateManaged true to simulate managed lifecycle + */ + public TestCommand(final Application application, final boolean simulateManaged) { + this(application, simulateManaged, null); + } + + /** + * Create command. + * + * @param application application instance + * @param simulateManaged true to simulate managed lifecycle + * @param modifiers configuration modifiers + */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") + public TestCommand(final Application application, final boolean simulateManaged, + final List> modifiers) { + super(application, "guicey-test", "Specific command to run guice context without jetty server"); + cleanupAsynchronously(); + configurationClass = application.getConfigurationClass(); + this.simulateManaged = simulateManaged; + this.modifiers = modifiers; + } + + @Override + protected void run(final Bootstrap bootstrap, + final Namespace namespace, + final C configuration) throws Exception { + // at this point only logging configuration performed + if (modifiers != null) { + ConfigOverrideUtils.runModifiers(configuration, modifiers); + } + super.run(bootstrap, namespace, configuration); + } + + @Override + protected void run(final Environment environment, final Namespace namespace, + final C configuration) throws Exception { + // simulating managed objects lifecycle support + // if managed lifecycle is not required, just prevent such objects registration, but + // preserve simulation itself as guicey application events rely on it + container = simulateManaged ? new ContainerLifeCycle() : new NoManagedContainerLifeCycle(); + environment.lifecycle().attach(container); + container.start(); + if (!simulateManaged) { + logger.info("NOTE: Managed lifecycle support disabled!"); + } + } + + /** + * Stop lifecycle. + */ + public void stop() { + if (container != null) { + try { + container.stop(); + } catch (Exception e) { + throw new IllegalStateException("Failed to stop managed objects container", e); + } + container.destroy(); + } + cleanup(); + } + + @Override + protected Class getConfigurationClass() { + return configurationClass; + } + + /** + * Custom container lifecycle with additional objects ignorance. It is important to presence lifecycle + * itsef due to {@link #addEventListener(java.util.EventListener)}, used for application start/stop detection + * (and some reports). + */ + public static class NoManagedContainerLifeCycle extends ContainerLifeCycle { + + @Override + public boolean addBean(final Object o) { + // ignore registrations (for Managed and LifeCycle objects) + return false; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java new file mode 100644 index 000000000..9b9be3522 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java @@ -0,0 +1,509 @@ +package ru.vyarus.dropwizard.guice.test; + +import com.google.common.base.Stopwatch; +import com.google.inject.Injector; +import com.google.inject.Key; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportBuilder; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.cmd.CommandRunBuilder; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.util.RunResult; +import ru.vyarus.dropwizard.guice.test.util.io.EchoStream; + +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +/** + * Utility class combining test-framework agnostic utilities. + *

          + *
        • {@link DropwizardTestSupport} factory + *
        • {@link GuiceyTestSupport} factory (same as previous but without web part starting) + *
        • {@link ClientSupport} factory (web client) + *
        • Guice-related utilities like {@link Injector} or beans lookup + *
        • Utility methods for running before and after methods in one call (useful for error situation testing). + *
        • Utilities for accessing context {@link io.dropwizard.testing.DropwizardTestSupport} object for both + * manually running tests (with run method below) or junit extensions
        • + *
        + * + * @author Vyacheslav Rusakov + * @since 09.02.2022 + */ +@SuppressWarnings("PMD.CouplingBetweenObjects") +public final class TestSupport { + + private TestSupport() { + } + + /** + * Generic builder to build and run application (core or web). This is the most flexible way to build or run + * the support object (with all possible options). In simple cases, prefer direct methods + * like {@link #coreApp(Class, String, String...)} or {@link #runCoreApp(Class, String, String...)} (all + * these methods are builder shortcuts). + *

        + * Should be useful for testing without a custom test framework integration as it provides lifecycle listener + * support to simplify setup and cleaup actions. + * + * @param app application class + * @param configuration type + * @return test support object builder + */ + public static TestSupportBuilder build(final Class> app) { + return new TestSupportBuilder<>(app); + } + + /** + * Builder (similar to {@link #build(Class)}) for testing commands. In contrast to the application support object, + * command testing is a one-shot operation. That's why command runner would intercept all used dropwizard objects + * during execution (including injector, if it was created) for assertions after command shutdown. + *

        + * Could be used to test any command: {@link io.dropwizard.core.cli.Command}, + * {@link io.dropwizard.core.cli.ConfiguredCommand} or {@link io.dropwizard.core.cli.EnvironmentCommand}. + * But injector would be created only in case of environment command. Other objects, like + * {@link io.dropwizard.core.Configuration} or {@link io.dropwizard.core.setup.Environment} might also be absent + * (if they were not created). + *

        + * Configuration is managed completely the same wat as with {@link io.dropwizard.testing.DropwizardTestSupport} + * object. So there is no need to put a configuration fila path inside command (it would be applied automatically, + * if provided in builder). + *

        + * Suitable for application startup errors testing: in case of successful startup application would shut down + * immediately, preventing test freezing. + * + * @param app application to test command + * @param configuration type + * @return builder to configure command execution + */ + public static CommandRunBuilder buildCommandRunner( + final Class> app) { + return new CommandRunBuilder<>(app); + } + + /** + * Obtains a context support object used by test application running in the current thread. + * Works for manual runs (using run* methods below) and junit extension runs. + * + * @param configuration type + * @return support object running application + * @throws java.lang.IllegalStateException if context support object is not registered for the current thread + */ + public static DropwizardTestSupport getContext() { + return TestSupportHolder.getContext(); + } + + /** + * Obtains a context client instance used by test application running in the current thread. + * Works for manual runs (using run* methods below) and junit extension runs. + * + * @return client instance (in case of junit extensions - same instance) + * @throws java.lang.IllegalStateException if context support object is not registered for the current thread + */ + public static ClientSupport getContextClient() { + return TestSupportHolder.getClient(); + } + + /** + * Creates {@link DropwizardTestSupport} instance for application configured from configuration file. + * {@link DropwizardTestSupport} starts complete dropwizard application including web part. Suitable + * for testing rest or servlet endpoints. For web-less application start see + * {@link #coreApp(Class, String, String...)}. + *

        + * Note: this is just a most common use-case, for more complex cases instantiate object manually using + * different constructor. + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @return dropwizard test support instance + */ + public static DropwizardTestSupport webApp( + final Class> appClass, + final @Nullable String configPath, + final String... overrides) { + return build(appClass) + .config(configPath) + .configOverrides(overrides) + .buildWeb(); + } + + /** + * Creates {@link GuiceyTestSupport} instance for application configured from configuration file. It is + * pre-configured {@link DropwizardTestSupport} instance (derivative class) starting only core application + * part (guice context) without web part. Suitable for testing core logic. + *

        + * Note: this is just a most common use-case, for more complex cases instantiate object manually using + * different constructor. + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @return guicey test support instance + */ + public static GuiceyTestSupport coreApp( + final Class> appClass, + final @Nullable String configPath, + final String... overrides) { + return build(appClass) + .config(configPath) + .configOverrides(overrides) + .buildCore(); + } + + /** + * Factory method for creating a helper web client. The client is aware of dropwizard configuration and allows + * easy calling main/rest/admin contexts. It could also be used as a generic web client (for remote endpoints + * calls). + *

        + * Note that instance must be closed after usage, for example, with try-with-resources: + * {@code try(ClientSupport client = TestSupport.webClient(support)) {...}}. + * + * @param support test support object (dropwizard or guicey) + * @return client support instance + */ + public static ClientSupport webClient(final DropwizardTestSupport support) { + return new ClientSupport(support); + } + + /** + * Helper web client creation with custom jersey client factory (to configure client differently). + * Note that {@link ClientSupport} is still useful in this case because it automatically constructs + * urls for tested application (based on configuration). + * + * @param support test support object (dropwizard or guicey) + * @param factory configuration factory + * @return client support instance + */ + public static ClientSupport webClient(final DropwizardTestSupport support, final TestClientFactory factory) { + return new ClientSupport(support, factory); + } + + /** + * @param support test support object (dropwizard or guicey) + * @return application injector instance + */ + public static Injector getInjector(final DropwizardTestSupport support) { + return InjectorLookup.getInjector(support.getApplication()) + .orElseThrow(() -> new IllegalStateException("Guice injector not available")); + } + + /** + * Shortcut for accessing guice beans. + * + * @param support test support object (dropwizard or guicey) + * @param type target bean type + * @param bean type + * @return bean instance + */ + public static T getBean(final DropwizardTestSupport support, final Class type) { + return getBean(support, Key.get(type)); + } + + /** + * Shortcut for accessing guice beans. + * + * @param support test support object (dropwizard or guicey) + * @param key binding key + * @param bean type + * @return bean instance + */ + public static T getBean(final DropwizardTestSupport support, final Key key) { + return getInjector(support).getInstance(key); + } + + /** + * Shortcut method to apply field injections into target object instance. Useful to initialize test class + * fields (under not supported test frameworks). + * + * @param support test support object (dropwizard or guicey) + * @param target target instance to inject beans + * @return injection duration (could be useful for slow injection detection) + */ + public static Duration injectBeans(final DropwizardTestSupport support, final Object target) { + final Stopwatch timer = Stopwatch.createStarted(); + getInjector(support).injectMembers(target); + return timer.elapsed(); + } + + /** + * Shortcut for {@link #run(io.dropwizard.testing.DropwizardTestSupport, + * ru.vyarus.dropwizard.guice.test.TestSupport.RunCallback)}. + * + * @param support test support instance + * @param configuration type + * @return result object with the main objects for assertions (for example, to examine configuration) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static RunResult run(final DropwizardTestSupport support) throws Exception { + return run(support, injector -> new RunResult<>(getContext(), injector)); + } + + /** + * Shortcut for {@link #run(io.dropwizard.testing.DropwizardTestSupport, + * ru.vyarus.dropwizard.guice.test.client.TestClientFactory, + * ru.vyarus.dropwizard.guice.test.TestSupport.RunCallback)}. + * + * @param callback callback (may be null) + * @param result type + * @param support test support instance + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T run(final DropwizardTestSupport support, + @Nullable final RunCallback callback) throws Exception { + return run(support, null, callback); + } + + + /** + * Normally, {@link DropwizardTestSupport#before()} and {@link DropwizardTestSupport#after()} methods are called + * separately. This method is a shortcut mostly for errors testing when {@link DropwizardTestSupport#before()} + * assumed to fail to make sure {@link DropwizardTestSupport#after()} will be called in any case. + * + * @param callback callback (may be null) + * @param result type + * @param support test support instance + * @param clientFactory custom client factory for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T run(final DropwizardTestSupport support, + final @Nullable TestClientFactory clientFactory, + final @Nullable RunCallback callback) throws Exception { + try { + TestSupportHolder.setContext(support, clientFactory); + support.before(); + return callback != null ? callback.run(getInjector(support)) : null; + } finally { + support.after(); + TestSupportHolder.reset(); + } + } + + /** + * Shortcut for web application startup. + * + * @param appClass application class + * @param configuration type + * @return result object with the main objects for assertions (for example, to examine configuration) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static RunResult runWebApp( + final Class> appClass) throws Exception { + return runWebApp(appClass, (String) null); + } + + /** + * Shortcut for web application startup. + * + * @param appClass application class + * @param callback callback to execute while application started (may be null) + * @param configuration type + * @param result type + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T runWebApp( + final Class> appClass, + final @Nullable RunCallback callback) throws Exception { + return runWebApp(appClass, null, callback); + } + + /** + * Shortcut for web application startup with configuration (optional). + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @return test support object used for execution (for example, to examine configuration) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static RunResult runWebApp( + final Class> appClass, + final @Nullable String configPath, + final String... overrides) throws Exception { + return run(webApp(appClass, configPath, overrides)); + } + + /** + * Shortcut for web application startup test (replacing + * {@code TestSupport.execute(TestSupport.webApp(App.class, path), callback)}). + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param callback callback to execute while application started (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @param result type + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T runWebApp(final Class> appClass, + final @Nullable String configPath, + final @Nullable RunCallback callback, + final String... overrides) throws Exception { + return run(webApp(appClass, configPath, overrides), callback); + } + + /** + * Shortcut for core application startup. + * + * @param appClass application class + * @param configuration type + * @return result object with the main objects for assertions (for example, to examine configuration) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static RunResult runCoreApp( + final Class> appClass) throws Exception { + return runCoreApp(appClass, (String) null); + } + + /** + * Shortcut for core application startup. + * + * @param appClass application class + * @param callback callback to execute while application started (may be null) + * @param configuration type + * @param result type + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T runCoreApp( + final Class> appClass, + final @Nullable RunCallback callback) throws Exception { + return runCoreApp(appClass, null, callback); + } + + /** + * Shortcut for core application startup test (replacing + * {@code TestSupport.execute(TestSupport.coreApp(App.class, path))}). + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @return result object with the main objects for assertions (for example, to examine configuration) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static RunResult runCoreApp( + final Class> appClass, + final @Nullable String configPath, + final String... overrides) throws Exception { + return run(coreApp(appClass, configPath, overrides)); + } + + /** + * Shortcut for core application startup test (replacing + * {@code TestSupport.execute(TestSupport.coreApp(App.class, path), callback)}). + * + * @param appClass application class + * @param configPath configuration file path (absolute or relative to working dir) (may be null) + * @param callback callback to execute while application started (may be null) + * @param overrides config override values (in format "path: value") + * @param configuration type + * @param result type + * @return callback result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public static T runCoreApp(final Class> appClass, + final @Nullable String configPath, + final @Nullable RunCallback callback, + final String... overrides) throws Exception { + return run(coreApp(appClass, configPath, overrides), callback); + } + + /** + * Enables debug output for registered junit 5 extensions. Simple alias for: + * {@code System.setProperty("guicey.extensions.debug", "true")}. + *

        + * Alternatively, debug could be enabled on extension directly with debug option. + */ + public static void debugExtensions() { + System.setProperty(TestExtensionsTracker.GUICEY_EXTENSIONS_DEBUG, "true"); + } + + /** + * Simple utility to capture console output. Could be used to test application output in some situations. + *

        + * Captured output is duplicated in console (for visual assertions). + *

        + * Warning: due to System.in/out modification, tests using this method can't run concurrently! + * + * @param action action to record output + * @return captured output (out + err) + * @throws java.lang.Exception on action errors (exceptions bypassed) + */ + public static String captureOutput(final OutputCallback action) throws Exception { + final PrintStream originalOut = System.out; + final PrintStream originalErr = System.err; + + final EchoStream stdOut = new EchoStream(originalOut); + final EchoStream stdErr = new EchoStream(stdOut); + System.setOut(new PrintStream(stdOut, false, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(stdErr, false, StandardCharsets.UTF_8)); + + try { + action.run(); + return stdOut.toString(); + } finally { + System.setOut(originalOut); + System.setErr(originalErr); + } + } + + /** + * Callback interface used for utility run application methods in {@link TestSupport}. + *

        + * Use {@link #getContext()} to access the context support object and {@link #getContextClient()} to access + * the context client. + * + * @param result type + */ + @FunctionalInterface + public interface RunCallback { + + /** + * Execute custom logic while application started (using {@link DropwizardTestSupport} or + * {@link GuiceyTestSupport}). + * + * @param injector application injector + * @return value or null + * @throws Exception errors propagated + */ + T run(Injector injector) throws Exception; + } + + /** + * Callback for {@link #captureOutput(ru.vyarus.dropwizard.guice.test.TestSupport.OutputCallback)} method. + * Void method must declare thrown error to simplify testing. + */ + @FunctionalInterface + public interface OutputCallback { + + /** + * Called to execute actions (usually app run) and capture console output. + * + * @throws Exception on error + */ + void run() throws Exception; + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java similarity index 98% rename from src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java index b9ef98413..c62304e50 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/binding/BindingsOverrideInjectorFactory.java @@ -43,6 +43,9 @@ public class BindingsOverrideInjectorFactory implements InjectorFactory { private final Logger logger = LoggerFactory.getLogger(BindingsOverrideInjectorFactory.class); + /** + * Create a factory. + */ public BindingsOverrideInjectorFactory() { // assumed that factory registered as instance before test run and so thread will always use clear state OVERRIDING_MODULES.remove(); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/BaseBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/BaseBuilder.java new file mode 100644 index 000000000..2b542c502 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/BaseBuilder.java @@ -0,0 +1,283 @@ +package ru.vyarus.dropwizard.guice.test.builder; + +import com.google.common.base.Preconditions; +import io.dropwizard.configuration.ConfigurationSourceProvider; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.ConfigOverride; +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.HooksUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Base class for test support objects builders. + * + * @param configuration type + * @param builder type + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") +public abstract class BaseBuilder> { + /** + * Application class. + */ + protected final Class> app; + /** + * Configuration file path. + */ + protected String configPath; + /** + * Configuration source provider. + */ + protected ConfigurationSourceProvider configSourceProvider; + /** + * Configuration overrides. + */ + protected final Map> configOverrides = new HashMap<>(); + /** + * Configuration modifiers. + */ + protected final List> modifiers = new ArrayList<>(); + /** + * Configuration instance (instead of file). + */ + protected C configObject; + /** + * Configuration overrides property prefix. + */ + protected String propertyPrefix; + /** + * Rest context mapping. + */ + protected String restMapping; + + /** + * Create builder. + * + * @param app application class + */ + public BaseBuilder(final Class> app) { + this.app = app; + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param path configuration file path + * @return builder instance for chained calls + */ + public T config(final @Nullable String path) { + this.configPath = path; + return self(); + } + + /** + * Use configuration instance instead of configuration parsing from yaml file. When this is used, other + * configuration options must not be used (they can't be used, and an error would be thrown indicating incorrect + * usage). + * + * @param config pre-initialized configuration object + * @return builder instance for chained calls + */ + public T config(final @Nullable C config) { + this.configObject = config; + return self(); + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param provider configuration source provider + * @return builder instance for chained calls + */ + public T configSourceProvider(final @Nullable ConfigurationSourceProvider provider) { + this.configSourceProvider = provider; + return self(); + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param overrides config override values (in format "path: value") + * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) + */ + public T configOverrides(final String... overrides) { + for (String over : overrides) { + configOverride(over); + } + return self(); + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param override config override value (in format "path: value") + * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) + */ + public T configOverride(final @Nullable String override) { + if (override != null) { + final int idx = override.indexOf(':'); + Preconditions.checkState(idx > 0, + "Incorrect configuration override declaration: must be 'key: value', but found '%s'", override); + configOverride(override.substring(0, idx).trim(), override.substring(idx + 1).trim()); + } + return self(); + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param key configuration path + * @param value overriding value + * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) + */ + public T configOverride(final String key, final String value) { + return configOverride(key, () -> value); + } + + /** + * Must not be used if {@link #config(io.dropwizard.core.Configuration)} used. + * + * @param key configuration path + * @param value overriding value provider + * @return builder instance for chained calls + */ + public T configOverride(final String key, final Supplier value) { + this.configOverrides.put(key, value); + return self(); + } + + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + *

        + * Modifier is called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml ({@link #config(String)}) and instance + * ({@link #config(io.dropwizard.core.Configuration)}) based configurations. + *

        + * Method supposed to be used with lambdas and so limited for application configuration class. + * For generic configurations (based on configuration subclass or raw {@link io.dropwizard.core.Configuration}) + * use {@link #configModifiers(Class[])}. + * + * @param modifiers configuration modifiers + * @return builder instance for chained calls + */ + @SafeVarargs + public final T configModifiers(final ConfigModifier... modifiers) { + Collections.addAll(this.modifiers, modifiers); + return self(); + } + + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + *

        + * Modifier is called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml ({@link #config(String)}) and instance + * ({@link #config(io.dropwizard.core.Configuration)}) based configurations. + *

        + * Method is useful for generic modifiers (based on configuration subclass or raw + * {@link io.dropwizard.core.Configuration}). + * + * @param modifiers configuration modifiers + * @return builder instance for chained calls + */ + @SafeVarargs + public final T configModifiers(final Class>... modifiers) { + this.modifiers.addAll(ConfigOverrideUtils.createModifiers(modifiers)); + return self(); + } + + /** + * Dropwizard stored all provided configuration overriding values as system properties with provided prefix + * (or "dw." by default). If multiple tests run concurrently, they would collide on using the same system + * properties. It is preferred to specify test-unique prefix. + * + * @param prefix configuration override properties prefix + * @return builder instance for chained calls + */ + public T propertyPrefix(final @Nullable String prefix) { + this.propertyPrefix = prefix; + return self(); + } + + + /** + * Shortcut for hooks registration (method simply immediately registers provided hooks). + * + * @param hooks hook classes to install (nulls not allowed) + * @return builder instance for chained calls + */ + @SafeVarargs + public final T hooks(final Class... hooks) { + HooksUtil.register(HooksUtil.create(hooks)); + return self(); + } + + /** + * Shortcut for hooks registration (method simply immediately registers provided hooks). + * + * @param hooks hooks to install (nulls allowed) + * @return builder instance for chained calls + */ + public T hooks(final GuiceyConfigurationHook... hooks) { + HooksUtil.register(Arrays.asList(hooks)); + return self(); + } + + /** + * Collect configuration overrides objects. + * + * @param prefix custom configuration overrides prefix + * @return configuration overrides + */ + protected ConfigOverride[] prepareOverrides(final String prefix) { + final ConfigOverride[] override = new ConfigOverride[configOverrides.size() + (restMapping == null ? 0 : 1)]; + int i = 0; + for (Map.Entry> entry : configOverrides.entrySet()) { + override[i++] = ConfigOverride.config(prefix, + entry.getKey(), entry.getValue()); + } + if (restMapping != null) { + override[i] = ConfigOverrideUtils.overrideRestMapping(prefix, restMapping); + } + return override; + } + + /** + * Specifies rest mapping path. This is the same as specifying direct config override + * {@code "server.rootMapping: /something/*"}. Specified value would be prefixed with "/" and, if required + * "/*" applied at the end. So it would be correct to specify {@code restMapping = "api"} (actually set value + * would be "/api/*"). + *

        + * This option is only intended to simplify cases when custom configuration file is not yet used in tests + * (usually early PoC phase). It allows you to map servlet into application root in test (because rest is no + * more resided in root). When used with existing configuration file, this parameter will override file definition. + * + * @param restMapping rest mapping path + * @return builder instance for chained calls + */ + public T restMapping(final String restMapping) { + this.restMapping = restMapping; + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportBuilder.java new file mode 100644 index 000000000..e95744418 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportBuilder.java @@ -0,0 +1,392 @@ +package ru.vyarus.dropwizard.guice.test.builder; + +import com.google.common.base.MoreObjects; +import com.google.inject.Injector; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.server.AbstractServerFactory; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.RandomPortsListener; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Builder and runner for {@link io.dropwizard.testing.DropwizardTestSupport} and + * {@link ru.vyarus.dropwizard.guice.test.GuiceyTestSupport} objects. Allows using all available options. This builder + * should be suitable for cases when junit 5 extensions could not be used. + *

        + * Use {@link ru.vyarus.dropwizard.guice.test.TestSupport#build(Class)} to build instance. + *

        + * Builder is not supposed to be used for multiple runs: registered hooks will be applied only once. This limitation + * is not possible to avoid because builder could be used for support objects creation, which are not aware of + * hooks. So hooks could be registered globally only in time of addition to the builder. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 14.11.2023 + */ +@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") +public class TestSupportBuilder extends BaseBuilder> { + + private boolean randomPorts; + private final List> listeners = new ArrayList<>(); + private TestClientFactory factory = new DefaultTestClientFactory(); + + /** + * Create builder. + * + * @param app application type + */ + public TestSupportBuilder(final Class> app) { + super(app); + } + + /** + * Shortcut to enable random web ports. + * + * @return builder instance for chained calls + */ + public TestSupportBuilder randomPorts() { + return randomPorts(true); + } + + /** + * Use random http ports (applicable only for web). Useful to separate concurrent web instances runs. + * + * @param randomPorts true to use random ports + * @return builder instance for chained calls + */ + public TestSupportBuilder randomPorts(final boolean randomPorts) { + this.randomPorts = randomPorts; + return this; + } + + /** + * Custom client factory implementation used for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object + * creation (this special client class automatically constructs base urls for application under test, + * based on its configuration). + *

        + * Client instance could be accessed at any time (during test) with {@link TestSupportHolder#getClient()} + * + * @param factory factory instance + * @return builder instance for chained calls + */ + public TestSupportBuilder clientFactory(final TestClientFactory factory) { + this.factory = factory; + return this; + } + + /** + * Shortcut for {@link #clientFactory(ru.vyarus.dropwizard.guice.test.client.TestClientFactory)} to configure + * {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory}. The default + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} supports only HTTP 1.1 methods and have + * problem with PATCH method usage on jdk > 16. + * + * @return builder instance for chained calls + */ + public TestSupportBuilder apacheClient() { + return clientFactory(new ApacheTestClientFactory()); + } + + /** + * Listener used ONLY when builder run methods used! Listener may be used to perform additional initialization + * or cleanup before/after application execution. + * + * @param listener execution listener + * @return builder instance for chained calls + */ + public TestSupportBuilder listen(final TestListener listener) { + this.listeners.add(listener); + return this; + } + + /** + * Build a test support object with web services {@link io.dropwizard.testing.DropwizardTestSupport}. + * Method supposed to be used only by {@link ru.vyarus.dropwizard.guice.test.TestSupport} for support objects + * creation. Prefer direct run ({@link #runCore()}) method usage (used support object could be easily obtained + * with {@link ru.vyarus.dropwizard.guice.test.TestSupport#getContext()} in any place). + *

        + * IMPORTANT: listeners could not be used (because they are implemented as a custom run callback). + * Custom {@link ru.vyarus.dropwizard.guice.test.client.TestClientFactory} would also be lost! Use direct run + * methods to not lose them. + * + * @return guicey test support implementation + */ + public GuiceyTestSupport buildCore() { + if (!listeners.isEmpty()) { + throw new IllegalStateException("Listeners could be used only with run* methods."); + } + return buildCoreInternal(); + } + + /** + * Build a test support object with web services {@link io.dropwizard.testing.DropwizardTestSupport}. + * Method supposed to be used only by {@link ru.vyarus.dropwizard.guice.test.TestSupport} for support objects + * creation. Prefer direct run ({@link #runWeb()}) method usage (used support object could be easily obtained + * with {@link ru.vyarus.dropwizard.guice.test.TestSupport#getContext()} in any place). + *

        + * IMPORTANT: listeners could not be used (because they are implemented as a custom run callback). + * Custom {@link ru.vyarus.dropwizard.guice.test.client.TestClientFactory} would also be lost! Use direct run + * methods to not lose them. + * + * @return dropwizard test support implementation + */ + public DropwizardTestSupport buildWeb() { + if (!listeners.isEmpty()) { + throw new IllegalStateException("Listeners could be used only with run* methods."); + } + return buildWebInternal(); + } + + /** + * Start and stop application without web services. Mostly useful to test application startup errors + * (with proper application shutdown). + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @return action result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public RunResult runCore() throws Exception { + return runCore(injector -> new RunResult(TestSupport.getContext(), injector)); + } + + /** + * Start and stop application without web services. Provided action would be executed in time of application life. + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @param action action to execute while the application is running + * @param result type + * @return action result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public T runCore(final @Nullable TestSupport.RunCallback action) throws Exception { + return run(buildCoreInternal(), action); + } + + /** + * Start and stop application without web services. Provided action would be executed in time of application life. + * Does not simulate {@link io.dropwizard.lifecycle.Managed} objects lifecycle (start/stop would not be called). + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @return execution result (with all required objects for verification) + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public RunResult runCoreWithoutManaged() throws Exception { + return runCoreWithoutManaged(injector -> new RunResult(TestSupport.getContext(), injector)); + } + + /** + * Start and stop application without web services. Provided action would be executed in time of application life. + * Does not simulate {@link io.dropwizard.lifecycle.Managed} objects lifecycle (start/stop would not be called). + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @param action action to execute while the application is running + * @param result type + * @return action result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public T runCoreWithoutManaged(final @Nullable TestSupport.RunCallback action) throws Exception { + return run(buildCoreInternal().disableManagedLifecycle(), action); + } + + + /** + * Start and stop application with web services. Mostly useful to test application startup errors + * (with proper application shutdown). + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @return result action + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public RunResult runWeb() throws Exception { + return runWeb(injector -> new RunResult(TestSupport.getContext(), injector)); + } + + /** + * Start and stop application with web services. Provided action would be executed in time of application life. + *

        + * NOTE: method not supposed to be used for multiple calls. For example, registered hooks would only work + * on first execution. + * + * @param action action to execute while the application is running + * @param result type + * @return action result + * @throws Exception any appeared exception (throws may easily be added directly to test method and, without + * extra exception wrapper, we get exact exceptions as they would be thrown in real application) + */ + public T runWeb(final @Nullable TestSupport.RunCallback action) throws Exception { + return run(buildWebInternal(), action); + } + + // "unsafe" building (without listeners check) + private GuiceyTestSupport buildCoreInternal() { + final GuiceyTestSupport support; + if (configObject != null) { + if (configPath != null || !configOverrides.isEmpty() || configSourceProvider != null) { + throw new IllegalStateException("Configuration object can't be used together with yaml configuration"); + } + support = new GuiceyTestSupport<>(app, configObject); + } else { + final String prefix = MoreObjects.firstNonNull(propertyPrefix, "dw."); + support = new GuiceyTestSupport<>(app, configPath, configSourceProvider, prefix, prepareOverrides(prefix)); + } + support.configModifiers(modifiers); + if (randomPorts) { + support.addListener(new RandomPortsListener<>()); + } + + return support; + } + + // "unsafe" building (without listeners check) + private DropwizardTestSupport buildWebInternal() { + final DropwizardTestSupport support; + if (configObject != null && restMapping != null && !restMapping.isEmpty()) { + // rest mapping can't be applied with config override in case of a raw config object, + // so need to use modifier instead + configModifiers(config -> ((AbstractServerFactory) config.getServerFactory()) + .setJerseyRootPath(ConfigOverrideUtils.formatRestMapping(restMapping))); + } + final Function, Command> cmd = ConfigOverrideUtils.buildCommandFactory(modifiers); + if (configObject != null) { + if (configPath != null || !configOverrides.isEmpty() || configSourceProvider != null) { + throw new IllegalStateException("Configuration object can't be used together with yaml configuration"); + } + support = new DropwizardTestSupport<>(app, configObject, cmd); + } else { + final String prefix = MoreObjects.firstNonNull(propertyPrefix, "dw."); + support = new DropwizardTestSupport<>(app, configPath, configSourceProvider, + prefix, cmd, prepareOverrides(prefix)); + } + if (randomPorts) { + support.addListener(new RandomPortsListener<>()); + } + + return support; + } + + private T run(final DropwizardTestSupport support, + final @Nullable TestSupport.RunCallback callback) throws Exception { + return runWithListeners(support, callback); + } + + private T runWithListeners(final DropwizardTestSupport support, + final @Nullable TestSupport.RunCallback callback) throws Exception { + // setup (before run) + for (TestListener testListener : listeners) { + testListener.setup(support); + } + try { + // using TestSupport for running because this api was added before builder and can't be moved into + // builder (without breaking change) + return TestSupport.run(support, factory, injector -> { + // after app startup, before the main test logic run + for (TestListener testListener : listeners) { + testListener.run(support, injector); + } + try { + return callback != null ? callback.run(injector) : null; + } finally { + // after the main test logic run (app still running) + for (TestListener listener : listeners) { + listener.stop(support, injector); + } + } + }); + } finally { + // after application shutdown + for (TestListener listener : listeners) { + listener.cleanup(support); + } + } + } + + /** + * Listener for {@link ru.vyarus.dropwizard.guice.test.TestSupport#build(Class)} builder. Listener works only when + * builder run method used! Useful for test-specific setup and cleanup. Note that + * {@link ru.vyarus.dropwizard.guice.test.GuiceyTestSupport} extends + * {@link io.dropwizard.testing.DropwizardTestSupport}. Guicey support object does not provide any useful + * methods (in context of the builder), so it is appropriate to always use dropwizard support object. + *

        + * See {@link ru.vyarus.dropwizard.guice.test.TestSupport} utility methods if something guice-related is + * required. To access web client use {@link ru.vyarus.dropwizard.guice.test.TestSupport#getContextClient()} + * + * @param configuration type + */ + public interface TestListener { + /** + * Called before application startup. + * + * @param support initialized support object (not started) + * @throws Exception any errors pass through + */ + default void setup(final DropwizardTestSupport support) throws Exception { + // empty + } + + /** + * An application started, but test logic was not executed yet. Will not be called in case of + * application startup error. + *

        + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport} web client could be accessed with + * {@link ru.vyarus.dropwizard.guice.test.TestSupport#getContextClient()}. + * + * @param support started support object + * @param injector injector instance + * @throws Exception any errors pass through + */ + default void run(final DropwizardTestSupport support, final Injector injector) throws Exception { + //empty + } + + /** + * Called after test action (or after exception during action execution), but before application shutdown. + *

        + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport} web client could be accessed with + * {@link ru.vyarus.dropwizard.guice.test.TestSupport#getContextClient()}. + * + * @param support still started suport object + * @param injector injector instance + * @throws Exception any errors pass through + */ + default void stop(final DropwizardTestSupport support, final Injector injector) throws Exception { + // empty + } + + /** + * Called after application shutdown (including startup error case). + * + * @param support stopped support object + * @throws Exception any errors pass through + */ + default void cleanup(final DropwizardTestSupport support) throws Exception { + // empty + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportHolder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportHolder.java new file mode 100644 index 000000000..e05cc8e9e --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/builder/TestSupportHolder.java @@ -0,0 +1,145 @@ +package ru.vyarus.dropwizard.guice.test.builder; + +import com.google.common.base.Preconditions; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; + + +/** + * Holds {@link io.dropwizard.testing.DropwizardTestSupport} object during test application execution. + * Works for junit 5 extensions run and for manual runs with + * {@link ru.vyarus.dropwizard.guice.test.TestSupport#run(io.dropwizard.testing.DropwizardTestSupport, + * ru.vyarus.dropwizard.guice.test.TestSupport.RunCallback)} method (or builder run methods). + * + * @author Vyacheslav Rusakov + * @since 15.11.2023 + */ +public final class TestSupportHolder { + + private static final ThreadLocal SUPPORT = new ThreadLocal<>(); + + private TestSupportHolder() { + } + + /** + * Used to register a context support object. Intended to be used ONLY by guicey. + * + * @param support context support object + * @param clientFactory custom factory object for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} + * (may be null for default factory usage) + * @throws java.lang.IllegalStateException if any support object already bound in thread + */ + public static void setContext(final DropwizardTestSupport support, + final @Nullable TestClientFactory clientFactory) { + setContext(support, TestSupport.webClient(support, clientFactory)); + Preconditions.checkNotNull(support, "Support object can't be null"); + // No check for already bound because junit tests could be hierarchical + SUPPORT.set(new State(support, TestSupport.webClient(support, clientFactory), true)); + } + + /** + * Used to register a context support object. Intended to be used ONLY by guicey. + * + * @param support context support object + * @param client client support instance (from junit 5 extension context; null to create new client) + * @throws java.lang.IllegalStateException if any support object already bound in thread + */ + public static void setContext(final DropwizardTestSupport support, + final @Nullable ClientSupport client) { + Preconditions.checkNotNull(support, "Support object can't be null"); + // No check for already bound because junit tests could be hierarchical + final boolean manageClient = client == null; + SUPPORT.set(new State(support, manageClient ? TestSupport.webClient(support) : client, manageClient)); + } + + /** + * Obtain the test support object, used for test application execution (by junit 5 extension or with + * {@link ru.vyarus.dropwizard.guice.test.TestSupport#run(io.dropwizard.testing.DropwizardTestSupport, + * ru.vyarus.dropwizard.guice.test.TestSupport.RunCallback)} (or any derived method, like builder run methods). + *

        + * Use {@link #isContextSet()} to check context initialization + * + * @param configuration type + * @return context support object + * @throws java.lang.NullPointerException if test support context is not bound in thread + */ + @SuppressWarnings("unchecked") + public static DropwizardTestSupport getContext() { + Preconditions.checkState(isContextSet(), "Test support object not bound in thread"); + return SUPPORT.get().getSupport(); + } + + /** + * @return true if the support object is bound in thread, false otherwise + */ + public static boolean isContextSet() { + return SUPPORT.get() != null; + } + + /** + * @return context test web client (in case of junit extensions would be the same client as in extenion) + */ + public static ClientSupport getClient() { + Preconditions.checkState(isContextSet(), "Test support object not bound in thread"); + return SUPPORT.get().getClient(); + } + + /** + * Reset thread-bound state. + */ + public static void reset() { + final State state = SUPPORT.get(); + if (state != null) { + SUPPORT.remove(); + if (state.isManageClient()) { + try { + state.getClient().close(); + } catch (Exception ignored) { + // silent + } + } + } + } + + /** + * Thread-bound test support object, user for currently running application. + */ + private static class State { + private final DropwizardTestSupport support; + private final ClientSupport client; + private final boolean manageClient; + + State(final DropwizardTestSupport support, + final ClientSupport client, + final boolean manageClient) { + this.support = support; + this.client = client; + this.manageClient = manageClient; + } + + /** + * @return test support object + */ + public DropwizardTestSupport getSupport() { + return support; + } + + /** + * @return client instance + */ + public ClientSupport getClient() { + return client; + } + + /** + * @return true if the client managed by holder (must be closed) + */ + public boolean isManageClient() { + return manageClient; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ApacheTestClientFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ApacheTestClientFactory.java new file mode 100644 index 000000000..a7b3bf0a5 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ApacheTestClientFactory.java @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import io.dropwizard.testing.DropwizardTestSupport; +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.glassfish.jersey.client.JerseyClientBuilder; + +/** + * Use {@link Apache5ConnectorProvider} instead of default {@link org.glassfish.jersey.client.HttpUrlConnectorProvider}. + * + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +public class ApacheTestClientFactory extends DefaultTestClientFactory { + + @Override + protected void configure(final JerseyClientBuilder builder, final DropwizardTestSupport support) { + builder.getConfiguration().connectorProvider(new Apache5ConnectorProvider()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/DefaultTestClientFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/DefaultTestClientFactory.java new file mode 100644 index 000000000..a296e1ff7 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/DefaultTestClientFactory.java @@ -0,0 +1,124 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import io.dropwizard.jersey.jackson.JacksonFeature; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.ws.rs.core.Feature; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.glassfish.jersey.logging.LoggingFeature; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.util.MultipartCheck; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default client factory for {@link ru.vyarus.dropwizard.guice.test.ClientSupport}. Enables INFO logging of + * all requests and responses into {@link ru.vyarus.dropwizard.guice.test.ClientSupport} logger. + * Auto register multipart feature if it's available in classpath (through dropwizard-froms). + *

        + * By default, log all requests and responses into system out (console). This could be disabled with + * {@link #disableConsoleLog()} method (system property). + *

        + * NOTE: default {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} does not support PATCH method on + * jdk > 16 (requires additional --add-opens). To workaround it, use apache or connection provider. + *

        + * If client customization is required, extend this class and override + * {@link #configure(org.glassfish.jersey.client.JerseyClientBuilder, + * io.dropwizard.testing.DropwizardTestSupport)} method. + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory + * @since 15.11.2023 + */ +public class DefaultTestClientFactory implements TestClientFactory { + + /** + * System property name used to disable direct console logs. + */ + public static final String USE_LOGGER = "USE_LOGGER_FOR_CLIENT"; + + /** + * Disable client logs into system out. Instead, logs would go into + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport} logger. + */ + public static void disableConsoleLog() { + System.setProperty(USE_LOGGER, "true"); + } + + /** + * Enable client logs into system out. Could be used to revert {@link #disableConsoleLog()} action. + */ + public static void enableConsoleLog() { + System.clearProperty(USE_LOGGER); + } + + @Override + public JerseyClient create(final DropwizardTestSupport support) { + final JerseyClientBuilder builder = new JerseyClientBuilder() + .register(new JacksonFeature(support.getEnvironment().getObjectMapper())) + // log everything to simplify debug + .register(createLogger()) + .property(ClientProperties.CONNECT_TIMEOUT, 1000) + .property(ClientProperties.READ_TIMEOUT, 5000) + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); + // apply multipart support, if multipart jar is present in the classpath. + MultipartCheck.getMultipartFeatureClass().ifPresent(builder::register); + configure(builder, support); + return builder.build(); + } + + /** + * Configure logging feature. + * + * @return logging feature + */ + protected Feature createLogger() { + return LoggingFeature.builder() + .withLogger(System.getProperty(USE_LOGGER) != null + // use console log by default + ? Logger.getLogger(ClientSupport.class.getName()) : new ConsoleLogger()) + .verbosity(LoggingFeature.Verbosity.PAYLOAD_ANY) + .level(Level.INFO) + .build(); + } + + /** + * Provides the ability to customize default client in extending class. + * + * @param builder client builder (pre-configured) + * @param support dropwizard support instance (for accessing environment and configuration) + */ + protected void configure(final JerseyClientBuilder builder, final DropwizardTestSupport support) { + // empty + } + + /** + * "Hacked" logger to print everything directly into system out. This is required because in tests (almost + * certainly) logging would not be properly configured and so messages would be "invisible". + */ + @SuppressWarnings("PMD.SystemPrintln") + public static class ConsoleLogger extends Logger { + + /** + * Create a console logger. + */ + public ConsoleLogger() { + super(ClientSupport.class.getName(), null); + } + + @Override + public boolean isLoggable(final Level level) { + return true; + } + + @Override + public void log(final Level level, final String msg) { + System.out.println("\n[Client action]---------------------------------------------{"); + System.out.println(msg); + System.out.println("}----------------------------------------------------------\n"); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ResourceClient.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ResourceClient.java new file mode 100644 index 000000000..c26cd380f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/ResourceClient.java @@ -0,0 +1,253 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.UriBuilder; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; +import ru.vyarus.dropwizard.guice.test.client.builder.call.MultipartAwareCaller; +import ru.vyarus.dropwizard.guice.test.client.builder.call.MultipartArgumentHelper; +import ru.vyarus.dropwizard.guice.test.client.builder.call.RestCallAnalyzer; +import ru.vyarus.dropwizard.guice.url.resource.ResourceAnalyzer; +import ru.vyarus.dropwizard.guice.url.util.Caller; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +/** + * Specialized rest client for resource classes. Useful to construct target paths directly from resource class methods. + *

        + * For example: {@code client.method(mock -> mock.restMethod("queryParam").asVoid()))} + * It would resolve target HTTP method, target path (from method annotation) and apply query parameter + * (because of not null value provided for method parameter, annotated with {@link jakarta.ws.rs.QueryParam}). + *

        + * This simplifies test request building, making it almost refactoring-safely. Also, it makes simple navigation from + * test to called resource method. + *

        + * It is still possible to call methods by path with generic methods like {@link #get(String, Class, Object...)} + * or {@link #buildGet(String, Object...)}. + *

        + * {@inheritDoc} + * + * @param resource type + * @author Vyacheslav Rusakov + * @since 18.09.2025 + */ +public class ResourceClient extends TestClient> { + + private final Class resource; + + /** + * Create a resource client. + * + * @param root root path + * @param defaults defaults + * @param resource resource type + */ + public ResourceClient(final @Nullable Supplier root, + final @Nullable TestRequestConfig defaults, + final Class resource) { + super(root, defaults); + this.resource = resource; + } + + /** + * The same as {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller, Object)}, but provides a helper + * utility to easily stub multipart parameters (so these values could be used for request configuration). + *

        + * For the most common case + * {@code post(@FormDataParam("file") InputStream stream, @FormDataParam("file") + * FormDataContentDisposition fileDetail)}: + *

        {@code multipartMethod((instance, multipart) -> instance
        +     *             .post(multipart.fromClasspath("/some.txt"),
        +     *                   multipart.disposition("file", "some.txt"))}
        . + *

        + * it could be a single field: {@code post(@FormDataParam("file") FormDataBodyPart file)}: + *

        {@code multipartMethod((instance, multipart) -> instance
        +     *             .post(multipart.filePart("/some.txt"))}
        + * or to get file from classpath: + *
        {@code multipartMethod((instance, multipart) -> instance
        +     *             .post(multipart.streamPart("/some.txt"))}
        . + *

        + * There might be multiple files for the same field + * {@code post(@FormDataParam("file") List file)}: + *

        {@code multipartMethod((instance, multipart) -> instance
        +     *          .post(Arrays.asList(
        +     *              multipart.filePart("/some.txt"),
        +     *              multipart.filePart("/other.txt")))}
        . + *

        + * When the method only accepts content-disposition mapping, it would be also used with en empty file content + * (as file content is not required, preserve available data): + * {@code post(@FormDataParam("file") FormDataContentDisposition file)} + *

        {@code multipartMethod((instance, multipart) -> instance
        +     *           .post(multipart.disposition("file", "some.txt"))}
        . + *

        + * The method parameter could be a complete multipart object (with all fields inside): + * {@code post(FormDataMultiPart multiPart)}: for such cases there is a special builder: + *

        {@code multipartMethod((instance, multipart) -> instance
        +     *           .post(multipart.multipart()
        +     *                  .field("foo", "val")
        +     *                  .file("file1", "/some.txt)
        +     *                  .stream("file2", "/other.txt)
        +     *                  .build())}
        + *

        + * It is not required to specify all parameters: you may use null on any of them (it is just a one way to + * configure request). + *

        + * Note that it is not required to declare fields like this! You can always prepare entity manually and use + * caller with body {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller, Object)}. + * + * @param caller multipart method caller + * @return builder instance for chained calls + */ + public TestClientRequestBuilder multipartMethod(final MultipartAwareCaller caller) { + return this.method(instance -> caller.call(instance, new MultipartArgumentHelper())); + } + + /** + * Configure request from resource method call. See + * {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller, Object)}. + * + * @param caller resource method caller + * @return pre-configured request builder instance + */ + public TestClientRequestBuilder method(final Caller caller) { + return method(caller, null); + } + + /** + * Configure a request from the resource method call. The main idea here: request could be configured + * automatically from the resource method annotations. Arguments for annotated parameters could be used to + * provide default values. + *

          + *
        • target path resolved from method {@link jakarta.ws.rs.Path}
        • + *
        • target method resolved from method annotation (like {@link jakarta.ws.rs.GET})
        • + *
        • analyze provided method arguments to get values for annotated parameters (like + * {@link jakarta.ws.rs.QueryParam}, {@link jakarta.ws.rs.PathParam}, etc.)
        • + *
        • build form body from not null {@link jakarta.ws.rs.FormParam} and + * {@link org.glassfish.jersey.media.multipart.FormDataParam}
        • + *
        + *

        + * All these configuration values could be overridden with a received builder. Also, put null on arguments + * that should not be configured automatically. + *

        + * Note that by default requests are logged, so the correctness of the target url could be verified easily. + *

        + * Might also include sub resource call, if sub resource locator method returns resource instance: + * {@code resource.sub(args).method(args)} + * + * @param caller resource method caller + * @param body request body (form param ignored for POST if body provided) + * @return pre-configured request builder instance + */ + public TestClientRequestBuilder method(final Caller caller, final @Nullable Object body) { + return RestCallAnalyzer.configure(this, caller, body); + } + + /** + * Create request builder from resource method: automatic configuration of targe path and http method selection. + *

        + * For more advanced defaults configuration see {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller)} + * (it will also apply configuration by provided method parameter values). + *

        + * If multiple methods selected, will select method + * without arguments. If there is no matching no-args method found throws exception (about multiple methods found). + * This no-args behavior is required to comply with arguments-based search (it would be otherwise impossible + * to search for no-args method). + * + * @param method method name to call (there must be only one such method) + * @return request builder instance + * @throws java.lang.IllegalStateException if unique method not found + */ + public TestClientRequestBuilder method(final String method) { + return method(method, null); + } + + /** + * Create request builder from resource method: automatic configuration of targe path and http method selection. + *

        + * For more advanced defaults configuration see {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller)} + * (it will also apply configuration by provided method parameter values). + * + * @param method method name to call (there must be only one such method) + * @return request builder instance + */ + public TestClientRequestBuilder method(final Method method) { + return method(method, null); + } + + /** + * Create request builder from resource method: automatic configuration of targe path and http method selection. + *

        + * For more advanced defaults configuration see {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller, Object)} + * (it will also apply configuration by provided method parameter values). + * + * @param method method name to call (there must be only one such method) + * @param body (optional) request body (everything except {@link jakarta.ws.rs.client.Entity} converted to JSON) + * @return request builder instance + * @throws java.lang.IllegalStateException if jersey annotations aren't found on the method + */ + public TestClientRequestBuilder method(final Method method, final @Nullable Object body) { + ResourceAnalyzer.validateResourceMethod(resource, method); + final Method annotated = ResourceAnalyzer.findAnnotatedMethod(method); + final UriBuilder builder = UriBuilder.newInstance(); + builder.path(annotated); + + final String httpMethod = ResourceAnalyzer.findHttpMethod(annotated); + return build(httpMethod, builder.toTemplate(), body); + } + + /** + * Create request builder from resource method: automatic configuration of targe path and http method selection. + *

        + * For more advanced defaults configuration see {@link #method(ru.vyarus.dropwizard.guice.url.util.Caller, Object)} + * (it will also apply configuration by provided method parameter values). + *

        + * If multiple methods selected, will select the method without arguments. If there is no matching no-args method + * found, throws exception (about multiple methods found). + * + * @param method method name to call (there must be only one such method) + * @param body (optional) request body (everything except {@link jakarta.ws.rs.client.Entity} converted to JSON) + * @return request builder instance + * @throws java.lang.IllegalStateException if unique method not found + */ + public TestClientRequestBuilder method(final String method, final @Nullable Object body) { + final Method target = ResourceAnalyzer.findMethod(resource, method); + return method(target, body); + } + + @Override + public ResourceClient restClient(final Class resource) { + // to minimize silly mistakes + throw new UnsupportedOperationException("In context of resource, sub-resource client should be obtained " + + "with subResourceClient() method which ignores sub-resource @Path annotation (not used in " + + "sub-resource path building)"); + } + + /** + * Create a sub client for the sub-resource. Sub resource path is resolved through the called locator method. + * Note: multiple locator methods could be called at once! + * + * @param caller locator method(s) caller + * @param subResource sub-resource type + * @param sub-resource type + * @return sub-resource client + */ + public ResourceClient subResourceClient(final Caller caller, final Class subResource) { + final String path = RestCallAnalyzer.getSubResourcePath(getResourceType(), caller); + // last class used for a resource type to get methods on + return new ResourceClient<>(() -> target(path), defaults, subResource); + } + + /** + * @return resource type + */ + public Class getResourceType() { + return resource; + } + + @Override + public String toString() { + return "Rest client for: " + resource.getSimpleName() + " (" + getRoot().getUri().toString() + ")"; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClient.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClient.java new file mode 100644 index 000000000..d42be516a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClient.java @@ -0,0 +1,1020 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.UriBuilder; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientDefaults; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; +import ru.vyarus.dropwizard.guice.url.util.RestPathUtils; + +import java.net.URI; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Wrapper for {@link org.glassfish.jersey.client.JerseyClient}. Jersey client is a general-purpose client and + * this api specialized for tests. Also, the jersey request builder is split into two parts: target and request. + * This api unifies it into a single step, for simplicity. + *

        + * The "defaults" concept: some values (query params, headers, cookies, etc.) could be configured once + * for all client requests (this is usually handy for tests, for example, to unify authorization). + * See "default*" methods for details (for example, {@link #defaultHeader(String, Object)}). + * Defaults could be cleared with {@link #reset()} or printed into console with {@link #printDefaults()} + *

        + * There are multiple ways to perform a request: + *

          + *
        • Raw jersey target {@link #target(String, Object...)} (defaults does not apply!)
        • + *
        • Raw jersey builder {@link #request(String, Object...)} with applied defaults
        • + *
        • Shortcut methods (to execute a general call and receive the result) like + * {@link #get(String, Class, Object...)}, {@link #post(String, Object, Class, Object...)}, etc.
        • + *
        • Shortcut builders (for additional request configurations) like {@link #buildGet(String, Object...)}, + * {@link #buildPost(String, Object, Object...)}, etc
        • + *
        • Special builder for forms (multipart and urlencoded): {@link #buildForm(String, Object...)}
        • + *
        • General builder: {@link #build(String, String, Object, Object...)}
        • + *
        + *

        + * All "build*" methods return a custom builder object with all the same configuration options as the original + * jersey client builder, but all gathered in one place (easier to use). This builder could return either + * directly mapped object (with + * {@link TestClientRequestBuilder#as(Class)}) or a special response object wrapper with pre-defined assertions + * to simplify testing ({@link TestClientRequestBuilder#expectSuccess(Integer...)} or + * {@link TestClientRequestBuilder#expectFailure(Integer...)} or without response validation + * {@link TestClientRequestBuilder#invoke()}). + *

        + * All methods support (optional) string format for a path. For example, the call + * {@code client.get("/entity/%s", Entity.class, 12)} will use "/entity/12/" as target path. + *

        + * Examples: + *

        
        + *     TestClient client;
        + *
        + *     // jersey api (no defaults applied)
        + *     Entity res = client.target("/some/path").request().get(Entity.class);
        + *
        + *     // jersey api (with defaults applied)
        + *     Entity res = client.request("/some/path").get(Entity.class);
        + *
        + *     // shortcut method
        + *     Entity res = client.get("/some/path", Entity.class);
        + *
        + *     // shortcut with composite entity type (note diamond operator usage):
        + *     List<Entity> res = client.get("/some/path", new GenericType<>() {});
        + *
        + *     // builder method
        + *     Entity res = client.buildGet("/some/path")
        + *              .header("Something", "Value")
        + *              .invoke(Entity.class);
        + *
        + *     // builder with response assertions ():
        + *     Entity res = client.buildGet("/some/path")
        + *              // error thrown if response is not success with (optional) exact status validation
        + *              .expectSuccess(200)
        + *              // assertion error if value will not match or cookie absent
        + *              .assertCookie("Name", "Value")
        + *              // assertion error if value will not match or header absent
        + *              .assertHeader("Something", "Value")
        + *              // map entity
        + *              .as(EntityClass)
        + *
        + *     // defaults demo:
        + *     client.defaultHeader("Authorization", "Bearer 1234567890");
        + *
        + *     // request would contain the default header
        + *     Entity res = client.get("/some/path", Entity.class);
        + * 
        + *

        + * The main idea behind chained assertions is redundant variables avoidance in test. + *

        + * Note that builder method return {@link java.lang.AutoCloseable} object (same as {@link jakarta.ws.rs.core.Response}) + * because a response object must be closed (response is closed when response body is consumed). + * Because of this your IDE could warn you that the result of, for example, {@link TestClientRequestBuilder#invoke()} + * must be used with "try-with-resources" statement. In most cases, you can ignore this warning as guicey tracks all + * such wrapped responses and would close it after a test application shutdown. + *

        + * Builder api does not hide jersey api: if required, you can modify target directly with + * {@link #defaultPathConfiguration(java.util.function.Function)} or jersey builder with + * {@link #defaultRequestConfiguration(java.util.function.Consumer)}. + *

        + * Builder supports jersey client properties ({@link #defaultProperty(String, Object)}) and extensions + * {@link #defaultRegister(Class)} and {@link #defaultRegister(Object)}. As an example, property could be used to + * disable redirects ({@link TestClientRequestBuilder#notFollowRedirects()} which is used automatically by + * {@link TestClientRequestBuilder#expectRedirect(Integer...)}). For a custom extension example see + * {@link TestClientRequestBuilder#noBodyMappingForVoid()} (which is used automatically by + * {@link TestClientRequestBuilder#asVoid()} to ignore response body mapping for void requests). + *

        + * The client could be used to build sub-clients. In this case all defaults will be inherited from the parent client. + * For example, we have a general rest client, but our particular test would check only one resource. In this case + * we can create a sub-client for the resource path to avoid it in all method calls: + *

        
        + *     TestClient api;
        + *     TestClient subRest = api.subClient("/resource/{param}/path")
        + *              .defaultPathParam("param", 123);
        + *      // this would call "/resource/123/path/method"
        + *     Entity res = subRest.get("/method", Entity.class);
        + * 
        + *

        + * {@link TestClient} is a general client class, but there are special client classes for rest (extending it): + * {@link ru.vyarus.dropwizard.guice.test.client.ResourceClient} (could be obtained from the root) and + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport object (which is also a test client)}) + * + * @param actual client type + * @author Vyacheslav Rusakov + * @since 13.09.2025 + */ +@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.TooManyMethods"}) +public class TestClient> extends TestClientDefaults { + + private final Supplier root; + + /** + * Construct a client without a base root path and defaults. This constructor could be used ONLY by + * extending classes, which override {@link #getRoot()} (these are root classes providing initial integration + * with test or stub clients ({@link ru.vyarus.dropwizard.guice.test.ClientSupport} and + * {@link ru.vyarus.dropwizard.guice.test.rest.RestClient}) because it is not possible to provide + * root target in constructor) + * + * @param defaults default configurations + */ + public TestClient(@Nullable final TestRequestConfig defaults) { + this(null, defaults); + } + + /** + * Construct a jersey client wrapper. + * + * @param root root target supplier + * @param defaults (optional) defaults + */ + public TestClient(final @Nullable Supplier root, final @Nullable TestRequestConfig defaults) { + super(new TestRequestConfig(defaults)); + Preconditions.checkState(root != null || !getClass().equals(TestClient.class), + "Target supplier may not be null for direct TestClient object usage: it could be null only for " + + "classes, extending TestClient (because they override getRoot() method)"); + this.root = root; + } + + /** + * @return base URI for this client + */ + public URI getBaseUri() { + return getRoot().getUri(); + } + + // ------------------------------------------------------------------------ PURE JERSEY + + /** + * Creates a web target to call under testing. This is a pure jersey api method for generic cases: in most cases, + * it would be simplir to use provided shortcuts (like {@link #get(String, Object...)}). + *

        + * Example: {@code .target("/smth/12/other").request().buildGet().invoke()} + * String format: {@code .target("/smth/%s/other", 12).request().buildGet().invoke()} + *

        + * WARNING: any specified defaults do not affect this method! + * + * @param path target path, relative to client root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return jersey web target object + */ + public WebTarget target(final String path, final Object... args) { + final String target = path == null || path.isEmpty() ? "" : String.format(path, args); + return getRoot().path(target); + } + + /** + * Create request for provided target with all defaults applied. Use for generic cases when jersey api + * usage required. In other cases use provided shortcuts or builder methods. + * + * @param path target path, relative to client root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return request object, ready to be sent + */ + public Invocation.Builder request(final String path, final Object... args) { + return defaults.applyRequestConfiguration(target(path, args)); + } + + // ------------------------------------------------------------------------ SUB CLIENTS + + /** + * Construct a sub-client with the provided path. Useful when performing multiple tests for common base url + * (e.g. testing one resource methods). + *

        
        +     *    TestClient api;
        +     *    TestClient subRest = api.subClient("/resource/{param}/path")
        +     *             .defaultPathParam("param", 123);
        +     *    // this would call "/resource/123/path/method"
        +     *    Entity res = subRest.get("/method", Entity.class);
        +     * 
        + *

        + * All defaults, configured for the current client, will be inherited in a sub-client. If this is not required, + * just clean defaults after creation: {@code client.subClient("path").reset()}. + * + * @param path target client root path (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return new client with a different root path + */ + public TestClient subClient(final String path, final Object... args) { + Preconditions.checkState(!path.toLowerCase().startsWith("http"), + "Only sub urls relative to current client url could be used. For completely custom external " + + "client creation use ClientSupport.externalClient()"); + // client INHERITS current defaults + return new TestClient<>(() -> target(String.format(path, args)), defaults); + } + + /** + * Create a new sub-client for a specified path. Sub path is constructed using jersey builder. + * For example: {@code client.subClient(builder -> builder.path("path").path("sub"))}. + *

        + * This might be useful for constructing complex paths with matrix parameters inside. Note that the target client + * could be easily "typed" with {@link #asRestClient(Class)} method. Or use {@link #subClient(String, Object...)}. + *

        + * All defaults, configured for the current client, will be inherited in a sub-client. If this is not required, + * just clean defaults after creation: {@code client.subClient(...).reset()}. + * + * @param consumer uri builder configurator + * @return client with a constructed path (relative to the current client path) + */ + public TestClient subClient(final Consumer consumer) { + final UriBuilder uriBuilder = UriBuilder.newInstance(); + consumer.accept(uriBuilder); + return new TestClient<>(() -> target(uriBuilder.toString()), defaults); + } + + /** + * Create a new rest client with a custom path. + * + * @param consumer path builder + * @param resource resource type + * @param resource type + * @return rest client for provided resource + */ + public ResourceClient subClient(final Consumer consumer, final Class resource) { + final UriBuilder uriBuilder = UriBuilder.newInstance(); + consumer.accept(uriBuilder); + return new ResourceClient<>(() -> target(uriBuilder.toString()), defaults, resource); + } + + /** + * Create a new sub-client for a specified resource class (appends a resource path, obtained from + * {@link jakarta.ws.rs.Path} annotation, to the current client path). Method is useful when generic + * rest path must be "typed" with a resource type (to be able to call resource methods directly). + *

        + * In case of sub-resources, use {@link #subResourceClient(String, Class, Object...)} to properly specify + * sub-resource mapping path (from lookup method): + * {@code ResourceClient rest = client.subResourceClient("path", SubResource.class)}. + * IMPORTANT: this is NOT THE SAME: {@code client.subClient("path").restClient(SubResource.class)} because + * "restClient()" call would append path from resource, which is ignored for sub resources!. + *

        + * Defaults could be used to declare path parameter values: + * {@code ResourceClient rest = client.restClient(Resource.class).defaultPathParam("param", "value")} where + * a resource class path is like "/some/{param}/path". With the default path param, there would be no need to + * declare it for each request call. + *

        + * All defaults, configured for the current client, will be inherited in a sub-client. If this is not required, + * just clean defaults after creation: {@code client.subClient(ResClass.class).reset()}. + * + * @param resource resource class one to build a path for + * @return resource client (with a resource path, relative to the current client path) + * @param resource type + */ + public ResourceClient restClient(final Class resource) { + final String target = RestPathUtils.getResourcePath(resource); + // last class used for a resource type to get methods on + return new ResourceClient<>(() -> target(target), defaults, resource); + } + + /** + * Create a sub client for the sub-resource. + *

        + * IMPORTANT: Path, declared on sub-resource class is ignored! Only lookup method path is counted. + * For example, {@code @Path("/sub") SubResource something() {...}} means all sub resource methods would be + * available on "/sub/*". + * + * @param path sub-resource mapping path (from sub-resource method; could contain String.format() + * placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @param subResource sub-resource + * @param sub-resource type + * @return sub-resource client + */ + public ResourceClient subResourceClient(final String path, final Class subResource, + final Object... args) { + final String target = String.format(path, args); + // last class used for a resource type to get methods on + return new ResourceClient<>(() -> target(target), defaults, subResource); + } + + /** + * Cast current path as provided resource (full!) path. Use-case: resources were mapped on non-standard path + * (admin context resources or internal resource mappings). + *

        + * IMPORTANT: this call will ignore resource {@code @Path} annotation - it assumes that the current path is + * already a resource path. + * + * @param resource resource type + * @param resource type + * @return rest client for provided resource + */ + public ResourceClient asRestClient(final Class resource) { + return new ResourceClient<>(() -> target("/"), defaults, resource); + } + + // ------------------------------------------------------------------------ REQUEST SHORTCUTS + + /** + * GET call shortcut. Almost the same as jersey {@code client.target(path).request().get(Void.class)}: + *

        + * To send a form (urlencoded) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildGet().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        • Response body is ignored ({@link TestClientRequestBuilder#noBodyMappingForVoid()})
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildGet(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code client.get("/smth/%s/other", 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + */ + public void get(final String path, final Object... args) { + get(path, Void.class, args); + } + + /** + * GET call shortcut. Almost the same as jersey {@code client.target(path).request().get(Some.class)}: + *

        + * To send a form (urlencoded) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildGet().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildGet(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.get("/smth/%s/other", Some.class, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R get(final String path, final @Nullable Class result, final Object... args) { + return handleShortcut(buildGet(path, args), result); + } + + /** + * GET call shortcut. Almost the same as jersey + * {@code client.target(path).request().get(new GenericType>(){})}: + *

        + * To send a form (urlencoded) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildGet().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildGet(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code List res = client.get("/smth/%s/other", new GenericType<>() {}, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R get(final String path, final @Nullable GenericType result, final Object... args) { + return handleShortcut(buildGet(path, args), result); + } + + /** + * POST call shortcut. Almost the same as jersey {@code client.target(path).request().post(entity, Void.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * For forms (urlencoded and multipart) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).buildPost().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        • Response body is ignored ({@link TestClientRequestBuilder#noBodyMappingForVoid()})
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPost(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code client.post("/smth/%s/other", object, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + */ + public void post(final String path, final @Nullable Object body, final Object... args) { + post(path, body, Void.class, args); + } + + /** + * POST call shortcut. Almost the same as jersey {@code client.target(path).request().post(entity, Some.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * For forms (urlencoded and multipart) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildPost().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPost(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.post("/smth/%s/other", object, Some.class, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R post(final String path, final @Nullable Object body, final @Nullable Class result, + final Object... args) { + return handleShortcut(buildPost(path, body, args), result); + } + + /** + * POST call shortcut. Almost the same as jersey + * {@code client.target(path).request().post(entity, new GenericType>(){}))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * For forms (urlencoded and multipart) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildPost().invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPost(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code List res = client.post("/smth/%s/other", object, new GenericType<>(){}, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R post(final String path, final @Nullable Object body, final @Nullable GenericType result, + final Object... args) { + return handleShortcut(buildPost(path, body, args), result); + } + + /** + * PUT call shortcut. Almost the same as jersey {@code client.target(path).request().put(entity, Void.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        • Response body is ignored ({@link TestClientRequestBuilder#noBodyMappingForVoid()})
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPut(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code client.put("/smth/%s/other", object, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + */ + public void put(final String path, final Object body, final Object... args) { + put(path, body, Void.class, args); + } + + /** + * PUT call shortcut. Almost the same as jersey {@code client.target(path).request().put(entity, Some.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPut(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.put("/smth/%s/other", object, Some.class, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R put(final String path, final Object body, final @Nullable Class result, final Object... args) { + return handleShortcut(buildPut(path, body, args), result); + } + + /** + * PUT call shortcut. Almost the same as jersey + * {@code client.target(path).request().put(entity, new GenericType>(){}))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPut(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code List res = client.put("/smth/%s/other", object, new GenericType<>(){}, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R put(final String path, final Object body, final @Nullable GenericType result, + final Object... args) { + return handleShortcut(buildPut(path, body, args), result); + } + + /** + * PATCH call shortcut. Almost the same as jersey + * {@code client.target(path).request().build("PATCH").invoke(Void.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * WARNING: in integration tests (real http call, not stub) the jersey client would use + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} which have problems with PATCH calls on JDK > 16 + * (requires additional --add-opens). To workaround this + * {@link org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider} could be used. + * Guicey provides custom {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory} for using + * apache client, which could be enabled with shortcut in test extensions (for example, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp#apacheClient()}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        • Response body is ignored ({@link TestClientRequestBuilder#noBodyMappingForVoid()})
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPatch(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code client.patch("/smth/%s/other", object, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + */ + public void patch(final String path, final @Nullable Object body, final Object... args) { + patch(path, body, Void.class, args); + } + + /** + * PATCH call shortcut. Almost the same as jersey + * {@code client.target(path).request().build("PATCH").invoke(Some.class))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * WARNING: in integration tests (real http call, not stub) the jersey client would use + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} which have problems with PATCH calls on JDK > 16 + * (requires additional --add-opens). To workaround this + * {@link org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider} could be used. + * Guicey provides custom {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory} for using + * apache client, which could be enabled with shortcut in test extensions (for example, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp#apacheClient()}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPatch(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.patch("/smth/%s/other", object, Some.class, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R patch(final String path, final @Nullable Object body, final @Nullable Class result, + final Object... args) { + return handleShortcut(buildPatch(path, body, args), result); + } + + /** + * PATCH call shortcut. Almost the same as jersey + * {@code client.target(path).request().build("PATCH").invoke(new GenericType>(){}))}: + *

        + * If body is already an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * WARNING: in integration tests (real http call, not stub) the jersey client would use + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} which have problems with PATCH calls on JDK > 16 + * (requires additional --add-opens). To workaround this + * {@link org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider} could be used. + * Guicey provides custom {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory} for using + * apache client, which could be enabled with shortcut in test extensions (for example, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp#apacheClient()}). + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildPatch(String, Object, Object...)}. + *

        + * String format could be used for path formatting: + * {@code List res = client.patch("/smth/%s/other", object, new GenericType<>(){}, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body entity object (everything except {@link Entity} converted to JSON) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R patch(final String path, final @Nullable Object body, final @Nullable GenericType result, + final Object... args) { + return handleShortcut(buildPatch(path, body, args), result); + } + + /** + * DELETE call shortcut. Almost the same as jersey {@code client.target(path).request().delete(Void.class)}: + *

        + * According to spec, HTTP DELETE should not support body and so there are no shortcuts with body. If + * you need to send DELETE with body use general builder: + * {@code build(HttpMethod.DELETE, "/some/path", object).invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        • Response body is ignored ({@link TestClientRequestBuilder#noBodyMappingForVoid()})
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildDelete(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code client.delete("/smth/%s/other", 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + */ + public void delete(final String path, final Object... args) { + delete(path, Void.class, args); + } + + /** + * PUT call shortcut. Almost the same as jersey {@code client.target(path).request().put(entity, Some.class))}: + *

        + * According to spec, HTTP DELETE should not support body and so there are no shortcuts with body. If + * you need to send DELETE with body use general builder: + * {@code build(HttpMethod.DELETE, "/some/path", object).invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildDelete(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.delete("/smth/%s/other", Some.class, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R delete(final String path, final @Nullable Class result, final Object... args) { + return handleShortcut(buildDelete(path, args), result); + } + + /** + * DELETE call shortcut. Almost the same as jersey + * {@code client.target(path).request().delete(entity, new GenericType>(){}))}: + *

        + * According to spec, HTTP DELETE should not support body and so there are no shortcuts with body. If + * you need to send DELETE with body use general builder: + * {@code build(HttpMethod.DELETE, "/some/path", object).invoke(Some.class)}. + *

          + *
        • Exception thrown if the result is not successful (not 2xx)
        • + *
        • For {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} resource exception + * propagated if no exception mappers registered
        • + *
        + *

        + * Additional headers, query params, etc. could be provided with defaults + * (e.g. {@link #defaultHeader(String, Object)}). For additional request-specific configuration + * use {@link #buildDelete(String, Object...)}. + *

        + * String format could be used for path formatting: + * {@code Some res = client.delete("/smth/%s/other", new GenericType<>(){}, 12)} + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param result result type (null or Void to ignore response body) + * @param args variables for path placeholders (String.format() arguments) + * @param result type + * @return mapped result object or null (if class not declared) + */ + public R delete(final String path, final @Nullable GenericType result, final Object... args) { + return handleShortcut(buildDelete(path, args), result); + } + + // ------------------------------------------------------------------------ REQUEST BUILDERS + + /** + * Generic request builder. + *

        + * Body is not required. If body is an {@link jakarta.ws.rs.client.Entity} - it will be used as is, other objects + * would be converted to json entity ({@link Entity#json(Object)}). + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + * + * @param method http method + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param body optional request body (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + * @see jakarta.ws.rs.HttpMethod + */ + public TestClientRequestBuilder build(final String method, final String path, final @Nullable Object body, + final Object... args) { + return new TestClientRequestBuilder(target(path, args), method, + body != null ? getEntity(body) : null, defaults); + } + + /** + * GET request builder. + *

        + * Example usage: {@code buildGet("/path/%s/sub", 12}.header("A", 1).invoke(Some.class)} + *

        + * In simple cases use shortcut: {@link #get(String, Class, Object...)}. + *

        + * For (urlencoded) forms use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildGet().invoke(Some.class)}. + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + */ + public TestClientRequestBuilder buildGet(final String path, final Object... args) { + return build(HttpMethod.GET, path, null, args); + } + + /** + * POST request builder. + *

        + * Example usage: {@code buildPost("/path/%s/sub", object, 12}.header("A", 1).invoke(Some.class)} + *

        + * In simple cases use shortcut: {@link #post(String, Object, Class, Object...)}. + *

        + * For forms (urlencoded and multipart) use {@link #buildForm(String, Object...)}: + * {@code buildForm("/path/%s/sub", 12).param("A", 1)..buildPost().invoke(Some.class)}. + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param entity request body (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + */ + public TestClientRequestBuilder buildPost(final String path, final Object entity, final Object... args) { + return build(HttpMethod.POST, path, entity, args); + } + + /** + * PUT request builder. + *

        + * Example usage: {@code buildPut("/path/%s/sub", object, 12}.header("A", 1).invoke(Some.class)} + *

        + * In simple cases use shortcut: {@link #put(String, Object, Class, Object...)}. + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param entity request body (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + */ + public TestClientRequestBuilder buildPut(final String path, final Object entity, final Object... args) { + return build(HttpMethod.PUT, path, entity, args); + } + + /** + * PATCH request builder. + *

        + * Example usage: {@code buildPatch("/path/%s/sub", object, 12}.header("A", 1).invoke(Some.class)} + *

        + * In simple cases use shortcut: {@link #patch(String, Object, Class, Object...)}. + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + *

        + * WARNING: in integration tests (real http call, not stub) the jersey client would use + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} which have problems with PATCH calls on JDK > 16 + * (requires additional --add-opens). To workaround this + * {@link org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider} could be used. + * Guicey provides custom {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory} for using + * apache client, which could be enabled with shortcut in test extensions (for example, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp#apacheClient()}). + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param entity request body (everything except {@link Entity} converted to JSON) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + */ + public TestClientRequestBuilder buildPatch(final String path, final Object entity, final Object... args) { + return build(HttpMethod.PATCH, path, entity, args); + } + + /** + * DELETE request builder. + *

        + * Example usage: {@code buildDelete("/path/%s/sub", 12}.header("A", 1).invoke(Some.class)} + *

        + * In simple cases use shortcut: {@link #delete(String, Class, Object...)}. + *

        + * Defaults like {@link #defaultHeader(String, Object)} are applied. + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return request builder + */ + public TestClientRequestBuilder buildDelete(final String path, final Object... args) { + return build(HttpMethod.DELETE, path, null, args); + } + + /** + * Build urlencoded or multipart form for GET (only urlencoded) and POST. + *

        + * Important: multipart support requires an additional dependency: + * 'org.glassfish.jersey.media:jersey-media-multipart'. + *

        + * The type of the form is detected by provided values: if at least one value requires multipart, then + * a multipart entity is created, otherwise urlencoded entity created. Only urlencoded entity could be used with + * GET. Multipart form type could be explicitly forced with + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder#forceMultipart()}. + *

        + * Example urlencoded form usage: + *

        
        +     *     buildForm(...)
        +     *     .param("name1", value1)
        +     *     .param("name2", value2)
        +     *     .buildPost()
        +     *     .invoke()
        +     * 
        + *

        + * Example multipart form usage: + *

        
        +     *     buildForm(...)
        +     *     .param("name1", value1)
        +     *     .param("file", file)
        +     *     .buildPost()
        +     *     .invoke()
        +     * 
        + *

        + * Also, builder could be used just for entity building: + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder#buildEntity()} (for manual entity usage). + *

        + * Form build would automatically serialiaze java.util and java.time dates into String. To modify date + * formats use {@link #defaultFormDateFormatter(java.text.DateFormat)} and + * {@link #defaultFormDateTimeFormatter(java.time.format.DateTimeFormatter)} (or in builder directly: + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder#dateFormatter(java.text.DateFormat)}, + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder#dateTimeFormatter( + * java.time.format.DateTimeFormatter)}). + *

        + * See {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder#param(String, Object)} for more details + * about parameter values conversion. + *

        + * Use null path to build entity: {@code buildForm(null).param().buildEntity()}. + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return form builder + */ + public FormBuilder buildForm(final @Nullable String path, final Object... args) { + return new FormBuilder(target(path, args), defaults); + } + + @Override + public String toString() { + return "Client for: " + getRoot().getUri().toString(); + } + + /** + * Check if provided value is already an entity. + * + * @param entity value to check + * @return entity itself if value is an entity or json entity + */ + protected Entity getEntity(final Object entity) { + final Entity body; + if (entity == null || entity instanceof Entity) { + body = (Entity) entity; + } else { + body = Entity.json(entity); + } + return body; + } + + /** + * Subclasses could override this method: this is required because it is not always possible to provide + * the correct root target in the constructor (when this method is overridden root, provided in the constructor is + * null). + * + * @return root target + */ + protected WebTarget getRoot() { + // for direct usage + if (root != null) { + return root.get(); + } + // for extended classes which pass null as supplier + throw new UnsupportedOperationException("Implementing class must override this method it it can't provider " + + "a correct supplier in constructor"); + } + + private R handleShortcut(final TestClientRequestBuilder request, final @Nullable Class result) { + return handleShortcut(request, result == null ? null : new GenericType<>(result)); + } + + private R handleShortcut(final TestClientRequestBuilder request, final @Nullable GenericType result) { + // immediate result mapping with bypassing exceptions in rest stubs mode (if not exception mapper registered) + return request.as(result); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClientFactory.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClientFactory.java new file mode 100644 index 000000000..f7226c53f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/TestClientFactory.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import io.dropwizard.testing.DropwizardTestSupport; +import org.glassfish.jersey.client.JerseyClient; + +/** + * Factory for {@link org.glassfish.jersey.client.JerseyClient} instance creation for + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport}. + *

        + * Custom factory might be useful, for example, if multipart data support should be enabled or gzip decoding. + * + * @author Vyacheslav Rusakov + * @since 15.11.2023 + */ +@FunctionalInterface +public interface TestClientFactory { + + /** + * Creates client instance for{@link ru.vyarus.dropwizard.guice.test.ClientSupport} (once per support instance). + * Called lazily (only before jersey client is actually required to perform call), so the support object should be + * already initialized. + * + * @param support support object + * @return client instance + */ + JerseyClient create(DropwizardTestSupport support); +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilder.java new file mode 100644 index 000000000..4ef0c3f37 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilder.java @@ -0,0 +1,338 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import org.glassfish.jersey.innate.spi.MessageBodyWorkersSettable; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.FormParamsSupport; +import ru.vyarus.dropwizard.guice.test.client.util.MultipartCheck; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.MultipartSupport; + +import java.io.File; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Specialized builder to simplify the creation of urlencoded and multipart forms. This builder is just creating an + * entity passed into usual GET or POST builders (these are the only possible methods for http forms). + *

        + * Important: multipart support requires an additional dependency: 'org.glassfish.jersey.media:jersey-media-multipart'. + *

        + * The type of the form is detected by provided values: if at least one value requires multipart, then multipart + * entity is created, otherwise urlencoded entity created. Only urlencoded entity could be used with GET. + * Multipart form type could be explicitly forced with {@link #forceMultipart()}. + *

        + * Multipart values are: {@link java.io.File}, {@link java.io.InputStream}, or any + * {@link org.glassfish.jersey.media.multipart.BodyPart} (including prepared fild objects like + * {@link org.glassfish.jersey.media.multipart.FormDataBodyPart}. + *

        + * Example urlencoded form usage: + *

        
        + *     buildForm(...)
        + *     .param("name1", value1)
        + *     .param("name2", value2)
        + *     .buildPost()
        + *     .invoke()
        + * 
        + *

        + * Example multipart form usage: + *

        
        + *     buildForm(...)
        + *     .param("name1", value1)
        + *     .param("file", file)
        + *     .buildPost()
        + *     .invoke()
        + * 
        + *

        + * Also, builder could be used just for entity building: {@link #buildEntity()} (for manual entity usage). + *

        + * HTTP standard supports only GET and POST methods for forms, so there are no other shortcuts. + * + * @author Vyacheslav Rusakov + * @since 15.09.2025 + */ +public class FormBuilder { + + private final WebTarget target; + private final TestRequestConfig config; + private final Map formParams = new HashMap<>(); + + private boolean multipart; + + /** + * Create form builder. + * + * @param target form target + * @param config inherited defaults + */ + public FormBuilder(final WebTarget target, final TestRequestConfig config) { + this.target = target; + this.config = new TestRequestConfig(config); + } + + /** + * Normally, a form type is detected from the provided values: if at least one value requires multipart, then + * multipart type selected, otherwise urlencoded used. + *

        + * This option forces multipart type usage, even if it is not required according to provided values. + * + * @return builder instance for chained calls + */ + public FormBuilder forceMultipart() { + this.multipart = true; + return this; + } + + /** + * Java.util date formatter for formatting date parameters. + * + * @param formatter date formatter + * @return builder instance for chained calls + * @see #dateFormat(String) for short declaration + */ + public FormBuilder dateFormatter(final DateFormat formatter) { + this.config.formDateFormatter(formatter); + return this; + } + + /** + * Java.time formatter for formatting date parameters. + * + * @param formatter date formatter + * @return builder instance for chained calls + * @see #dateFormat(String) for short declaration + */ + public FormBuilder dateTimeFormatter(final DateTimeFormatter formatter) { + this.config.formDateTimeFormatter(formatter); + return this; + } + + /** + * Shortcut to configure both date formatters with the same pattern. + * + * @param format format + * @return builder instance for chained calls + * @see #dateFormatter(java.text.DateFormat) + * @see #dateTimeFormatter(java.time.format.DateTimeFormatter) + */ + @SuppressWarnings("PMD.SimpleDateFormatNeedsLocale") + public FormBuilder dateFormat(final String format) { + this.config.formDateFormatter(new SimpleDateFormat(format)); + this.config.formDateTimeFormatter(DateTimeFormatter.ofPattern(format)); + return this; + } + + /** + * Add form parameter. Multiple values could be provided with collection or array. + *

        + * {@link File} or {@link java.io.InputStream} or anything derived from + * {@link org.glassfish.jersey.media.multipart.BodyPart} would lead to multipart form creation. For multipart + * requests, value could be provided in form of manually constructed fiels, like + * {@link org.glassfish.jersey.media.multipart.FormDataMultiPart} (any body part implementation). + *

        + * For other value types, value is converted to string: + *

          + *
        • Date fields string conversion could be customized with date formatters (one for java.util and other for + * java.time api).
        • + *
        • Null values converted to ""
        • + *
        • Collections converted to a comma-separated string of values (converted to string)
        • + *
        • By default, call toString on provided object
        • + *
        + *

        + * Custom date formatters could be set as a default for all requests with: + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#defaultFormDateFormatter(java.text.DateFormat)} and + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#defaultFormDateTimeFormatter( + * java.time.format.DateTimeFormatter)}. + * Or for request with: {@link #dateFormatter(java.text.DateFormat)} and + * {@link #dateTimeFormatter(java.time.format.DateTimeFormatter)}. + *

        + * Multiple calls override previously declared parameter. + * + * @param name parameter name + * @param value parameter value + * @return builder instance for chained calls + */ + public FormBuilder param(final String name, final Object value) { + formParams.put(name, value); + return this; + } + + /** + * Same as {@link #param(String, Object)} but provides multiple parameters for the same name. + * + * @param name parameter name + * @param values parameter values + * @return builder instance for chained calls + */ + public FormBuilder param(final String name, final Object... values) { + return param(name, Arrays.asList(values)); + } + + /** + * Adds multiple form params. Multiple values could be provided with collection or array. + * + * @param params form params map. + * @return builder instance for chained calls + */ + public FormBuilder params(final Map params) { + formParams.putAll(params); + return this; + } + + /** + * Build POST request builder for a form. This is the same as calling + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#buildPost(String, Object, Object...)}, but with + * pre-build entity. + * + * @return POST request builder + */ + public TestClientRequestBuilder buildPost() { + return new TestClientRequestBuilder(target, HttpMethod.POST, buildEntity(), config); + } + + /** + * Build GET request builder for a form. This is the same as calling + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#buildGet(String, Object...)}, but with pre-build + * query params. + *

        + * Call will be failed if multipart form creation is required (which can't be handled with GET). + * + * @return GET request builder + */ + public TestClientRequestBuilder buildGet() { + Preconditions.checkState(!isMultipart(), "Multipart form can't be sent with GET"); + return new TestClientRequestBuilder(target, HttpMethod.GET, null, config) + .queryParams(buildQueryParams()); + } + + /** + * Build entity for manual usage in calls. + *

        + * For example, could be used in manual post call (as body): + * {@code post("/somePath/", form).invoke()}. + * + * @return entity (form body) + */ + public Entity buildEntity() { + Preconditions.checkState(!formParams.isEmpty(), "At least one form param is required"); + final boolean multipart = this.multipart || isMultipart(); + + if (!multipart) { + final Form form = new Form(); + formParams.forEach((s, o) -> { + if (o instanceof Collection) { + for (Object v : ((Collection) o)) { + form.param(s, FormParamsSupport.parameterToString(v, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + } else if (o.getClass().isArray()) { + for (Object v : ((Object[]) o)) { + form.param(s, FormParamsSupport.parameterToString(v, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + } else { + form.param(s, FormParamsSupport.parameterToString(o, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + }); + return Entity.form(form); + } + + MultipartCheck.requireEnabled(); + return MultipartSupport.buildMultipart(formParams, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter()); + } + + /** + * Build query parameters for manual usage in GET calls. + *

        + * For example: + * {@code buildGet("/somePath/").queryParams(params).invoke())} + *

        + * Parameters with multiple values would contain lists of values. + * + * @return parameters map to be used in GET request + */ + @SuppressWarnings("unchecked") + public Map buildQueryParams() { + Preconditions.checkState(!formParams.isEmpty(), "At least one form param is required"); + final Map result = new HashMap<>(); + formParams.forEach((s, o) -> { + if (o instanceof Collection) { + result.put(s, handleMultivalue((Collection) o)); + } else if (o.getClass().isArray()) { + result.put(s, handleMultivalue((Object[]) o)); + } else { + result.put(s, FormParamsSupport.parameterToString(o, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + }); + return result; + } + + @Override + public String toString() { + return "Form builder for: " + target.getUri().toString(); + } + + private List handleMultivalue(final Collection values) { + final List res = new ArrayList<>(); + for (Object v : values) { + res.add(FormParamsSupport.parameterToString(v, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + return res; + } + + private List handleMultivalue(final Object... values) { + final List res = new ArrayList<>(); + for (Object v : values) { + res.add(FormParamsSupport.parameterToString(v, + config.getConfiguredFormDateFormatter(), + config.getConfiguredFormDateTimeFormatter())); + } + return res; + } + + @SuppressWarnings("checkstyle:ReturnCount") + private boolean isMultipart() { + for (Object value : formParams.values()) { + if (isMultipart(value)) { + return true; + } + // case: multiple values with the same name + if (value instanceof Collection) { + for (Object v : ((Collection) value)) { + if (isMultipart(v)) { + return true; + } + } + } + } + return false; + } + + private boolean isMultipart(final Object value) { + return value instanceof File + || value instanceof InputStream + // includes BodyPart and other file mapping fields (not directly BodyPart to support case + // when multipart jar not declared) + || value instanceof MessageBodyWorkersSettable; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientDefaults.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientDefaults.java new file mode 100644 index 000000000..2d67f70df --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientDefaults.java @@ -0,0 +1,691 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import ru.vyarus.dropwizard.guice.test.client.TestClient; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Defaults configuration for {@link ru.vyarus.dropwizard.guice.test.client.TestClient}. Extracted to make + * client class more readable. + *

        + * Defaults could be declared for client to be applied in all clients. Sub-clients, created from this client + * will inherit these defaults. + * + * @param actual client type + * @author Vyacheslav Rusakov + * @since 23.09.2025 + */ +@SuppressWarnings({"PMD.TooManyMethods", "PMD.ExcessivePublicCount"}) +public abstract class TestClientDefaults> { + + /** + * Default configuration for all requests (and sub clients). + */ + protected final TestRequestConfig defaults; + + /** + * Creates new defaults instance. + * + * @param defaults default configuration for all requests (and sub clients) + */ + public TestClientDefaults(final TestRequestConfig defaults) { + this.defaults = defaults; + } + + /** + * Configure default header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param value header value + * @return builder instance for chained calls + */ + public T defaultHeader(final HttpHeader name, final Object value) { + return defaultHeader(name.toString(), value); + } + + /** + * Configure default header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param value header value + * @return builder instance for chained calls + */ + public T defaultHeader(final String name, final Object value) { + return defaultHeader(name, () -> value); + } + + /** + * Configure default header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param supplier header value supplier + * @return builder instance for chained calls + */ + public T defaultHeader(final HttpHeader name, final Supplier supplier) { + return defaultHeader(name.toString(), supplier); + } + + /** + * Configure default header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param supplier header value supplier + * @return builder instance for chained calls + */ + public T defaultHeader(final String name, final Supplier supplier) { + defaults.header(name, supplier); + return self(); + } + + /** + * Configure default query parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}). + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name query parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public T defaultQueryParam(final String name, final Object value) { + return defaultQueryParam(name, () -> value); + } + + /** + * Configure default query parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}). + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name query parameter name + * @param supplier value supplier (list or array for multiple values) + * @return builder instance for chained calls + */ + public T defaultQueryParam(final String name, final Supplier supplier) { + defaults.queryParam(name, supplier); + return self(); + } + + /** + * Configure default matrix parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}). + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name matrix parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public T defaultMatrixParam(final String name, final Object value) { + return defaultMatrixParam(name, () -> value); + } + + /** + * Configure default matrix parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}). + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name matrix parameter name + * @param supplier value supplier (list or array for multiple values) + * @return builder instance for chained calls + */ + public T defaultMatrixParam(final String name, final Supplier supplier) { + defaults.matrixParam(name, supplier); + return self(); + } + + /** + * Configure default path parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#resolveTemplateFromEncoded(String, Object)}). + *

        + * Note: parameter value assumed to be encoded to be able to specify matrix parameters (in the middle of the path): + * {@code pathParam("var", "/path;var=1;val=2")}. + * + * @param name parameter name + * @param value parameter value + * @return builder instance for chained calls + */ + public T defaultPathParam(final String name, final Object value) { + return defaultPathParam(name, () -> value); + } + + /** + * Configure default path parameter for all requests + * ({@link jakarta.ws.rs.client.WebTarget#resolveTemplateFromEncoded(String, Object)}). + * + * @param name parameter name + * @param supplier parameter value supplier + * @return builder instance for chained calls + */ + public T defaultPathParam(final String name, final Supplier supplier) { + defaults.pathParam(name, supplier); + return self(); + } + + /** + * Configure default jersey client property applied to all requests + * ({@link org.glassfish.jersey.client.JerseyClient#property(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name property name + * @param value property value + * @return builder instance for chained calls + * @see org.glassfish.jersey.client.ClientProperties + */ + public T defaultProperty(final String name, final Object value) { + return defaultProperty(name, () -> value); + } + + /** + * Configure default jersey client property applied to all requests + * ({@link org.glassfish.jersey.client.JerseyClient#property(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name property name + * @param supplier property value supplier + * @return builder instance for chained calls + * @see org.glassfish.jersey.client.ClientProperties + */ + public T defaultProperty(final String name, final Supplier supplier) { + defaults.property(name, supplier); + return self(); + } + + /** + * Configure default jersey client extension for all requests + * ({@link org.glassfish.jersey.client.JerseyClient#register(Class)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader}. + *

        + * Multiple registrations of the same class will be ignored (extensions tracked by type). + * + * @param type extension class + * @return builder instance for chained calls + */ + public T defaultRegister(final Class type) { + defaults.register(type); + return self(); + } + + /** + * Configure default jersey client extension for all requests + * ({@link org.glassfish.jersey.client.JerseyClient#register(Object)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader} (as instance). + *

        + * When called with different instances of the same class: only the latest registration will be used + * (extensions tracked by type). + * + * @param extension extensions instance + * @return builder instance for chained calls + */ + public T defaultRegister(final Object extension) { + defaults.register(extension); + return self(); + } + + /** + * Configure default jersey client extension for all requests + * ({@link org.glassfish.jersey.client.JerseyClient#register(Object)}). + * Supplier could return either class or extension instance. + *

        + * When called with different instances of the same class: only the latest registration will be used + * (extensions tracked by type). + * + * @param type extension type + * @param supplier extension instance or null (for class) + * @param extension type + * @return builder instance for chained calls + */ + public T defaultRegister(final Class type, final Supplier supplier) { + defaults.register(type, supplier); + return self(); + } + + /** + * Configure default java.util date formatter for form fields (used in + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#buildForm(String, Object...)}). + * + * @param formatter date formatter + * @return builder instance for chained calls + * @see #defaultFormDateFormat(String) for short declaration + */ + public T defaultFormDateFormatter(final DateFormat formatter) { + defaults.formDateFormatter(formatter); + return self(); + } + + /** + * Configure default java.time formatter for form fields (used in + * {@link ru.vyarus.dropwizard.guice.test.client.TestClient#buildForm(String, Object...)}). + * + * @param formatter date formatter + * @return builder instance for chained calls + * @see #defaultFormDateFormat(String) for short declaration + */ + public T defaultFormDateTimeFormatter(final DateTimeFormatter formatter) { + defaults.formDateTimeFormatter(formatter); + return self(); + } + + /** + * Shortcut to configure both date formatters with the same pattern. + * + * @param format format + * @return builder instance for chained calls + * @see #defaultFormDateFormatter(java.text.DateFormat) + * @see #defaultFormDateTimeFormatter(java.time.format.DateTimeFormatter) + */ + @SuppressWarnings("PMD.SimpleDateFormatNeedsLocale") + public T defaultFormDateFormat(final String format) { + defaults.formDateFormatter(new SimpleDateFormat(format)); + defaults.formDateTimeFormatter(DateTimeFormatter.ofPattern(format)); + return self(); + } + + /** + * Configure default cookie for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Multiple calls override previous value. + * + * @param name cookie name + * @param value cookie value + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public T defaultCookie(final String name, final String value) { + return defaultCookie(name, () -> new NewCookie.Builder(name).value(value).build()); + } + + /** + * Configure default cookie for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Use cookie builder: {@code new NewCookie.Builder(name).value(value).build()}. + *

        + * Multiple calls override previous value. + * + * @param cookie cookie value + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public T defaultCookie(final Cookie cookie) { + return defaultCookie(cookie.getName(), () -> cookie); + } + + /** + * Configure default cookie for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Use cookie builder: {@code new NewCookie.Builder(name).value(value).build()}. + *

        + * Multiple calls override previous value. + * + * @param name name + * @param supplier value suppler + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public T defaultCookie(final String name, final Supplier supplier) { + defaults.cookie(name, supplier); + return self(); + } + + /** + * Configure default Accept header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}). + *

        + * Multiple calls override previous value. + * + * @param accept media types required for response + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.MediaType + */ + public T defaultAccept(final String... accept) { + this.defaults.accept(accept); + return self(); + } + + /** + * Configure default Accept header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}). + *

        + * Multiple calls override previous value. + * + * @param accept media types required for response + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.MediaType + */ + public T defaultAccept(final MediaType... accept) { + final String[] array = Arrays.stream(accept).map(mediaType -> + RuntimeDelegate.getInstance().createHeaderDelegate(MediaType.class).toString(mediaType)) + .toArray(String[]::new); + return defaultAccept(array); + } + + /** + * Configure default Accept-Language header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}). + *

        + * Multiple calls override previous value. + * + * @param languages languages to accept + * @return builder instance for chained calls + */ + public T defaultLanguage(final String... languages) { + this.defaults.acceptLanguage(languages); + return self(); + } + + /** + * Configure default Accept-Language header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}). + *

        + * Multiple calls override previous value. + * + * @param languages languages to accept + * @return builder instance for chained calls + */ + public T defaultLanguage(final Locale... languages) { + final String[] res = Arrays.stream(languages).map(Locale::toString).toArray(String[]::new); + return defaultLanguage(res); + } + + /** + * Configure default Accept-Encoding header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptEncoding(String...)}). + *

        + * Multiple calls override previous value. + * + * @param encodings encodings to accept + * @return builder instance for chained calls + */ + public T defaultEncoding(final String... encodings) { + this.defaults.acceptEncoding(encodings); + return self(); + } + + /** + * Configure default Cache-Control header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control header value (string) to apply to request. + * @return builder instance for chained calls + */ + public T defaultCacheControl(final String cacheControl) { + this.defaults.cacheControl(cacheControl); + return self(); + } + + /** + * Configure default Cache-Control header for all requests + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control settings to apply to request. + * @return builder instance for chained calls + */ + public T defaultCacheControl(final CacheControl cacheControl) { + this.defaults.cacheControl(cacheControl); + return self(); + } + + /** + * Enable requests' debugging: print to console what defaults was configured (and where) and how + * a request object was configured. + * + * @param debug true to enable debug + * @return builder instance for chained calls + */ + public T defaultDebug(final boolean debug) { + this.defaults.debug(debug); + return self(); + } + + /** + * Configure default request {@link jakarta.ws.rs.client.WebTarget} modifier for all requests. + * + * @param modifier function, applying request target configuration + * @return builder instance for chained calls + */ + public T defaultPathConfiguration(final Function modifier) { + this.defaults.configurePath(modifier); + return self(); + } + + /** + * Configure default request {@link jakarta.ws.rs.client.Invocation.Builder} modifier for all requests. + * + * @param modifier consumer, applying request configuration + * @return builder instance for chained calls + */ + public T defaultRequestConfiguration(final Consumer modifier) { + this.defaults.configureRequest(modifier); + return self(); + } + + /** + * Could be used for manual appliance of defaults to externally constructed target: + * {@code Invocation.Builder builder = getDefaults().configure(target)}. + * + * @return a copy of client defaults + */ + public TestRequestConfig getDefaults() { + return new TestRequestConfig(defaults); + } + + /** + * @return if any default configured + */ + public boolean hasDefaults() { + return defaults.hasConfiguration(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default headers specified + */ + public boolean hasDefaultHeaders() { + return !defaults.getConfiguredHeaders().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default query params specified + */ + public boolean hasDefaultQueryParams() { + return !defaults.getConfiguredQueryParams().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default matrix params specified + */ + public boolean hasDefaultMatrixParams() { + return !defaults.getConfiguredMatrixParams().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default path params specified + */ + public boolean hasDefaultPathParams() { + return !defaults.getConfiguredPathParams().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default client properties specified + */ + public boolean hasDefaultProperties() { + return !defaults.getConfiguredProperties().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default client extensions specified + */ + public boolean hasDefaultExtensions() { + return !defaults.getConfiguredExtensions().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if cookies specified + */ + public boolean hasDefaultCookies() { + return !defaults.getConfiguredCookies().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default Accept header specified + */ + public boolean hasDefaultAccepts() { + return !defaults.getConfiguredAccepts().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default Accept-Language header specified + */ + public boolean hasDefaultLanguages() { + return !defaults.getConfiguredLanguages().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default Accept-Encoding header specified + */ + public boolean hasDefaultEncodings() { + return !defaults.getConfiguredEncodings().isEmpty(); + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default Cache header specified + */ + public boolean hasDefaultCacheControl() { + return defaults.getConfiguredCacheControl() != null; + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default date formatter (either one or both) specified for form builder + * @see #defaultFormDateFormatter(java.text.DateFormat) + * @see #defaultFormDateTimeFormatter(java.time.format.DateTimeFormatter) + */ + public boolean hasDefaultFormDateFormatter() { + return defaults.getConfiguredFormDateFormatter() != null + || defaults.getConfiguredFormDateTimeFormatter() != null; + } + + /** + * Could be used for verifications in tests to avoid defaults collide. + * + * @return true if default path or request customizers registered + * @see #defaultPathConfiguration(java.util.function.Function) + * @see #defaultRequestConfiguration(java.util.function.Consumer) + */ + public boolean hasDefaultCustomConfigurators() { + return !defaults.getConfiguredPathModifiers().isEmpty() + || !defaults.getConfiguredRequestModifiers().isEmpty(); + } + + /** + * @return true if debug output is enabled + * @see #defaultDebug(boolean) + */ + public boolean isDebugEnabled() { + return defaults.isDebugEnabled(); + } + + /** + * Reset configured defaults. + * + * @return rest client itself for chained calls + */ + public T reset() { + defaults.clear(); + return self(); + } + + /** + * Print configured defaults to console. + * + * @return builder instance for chained calls + */ + @SuppressWarnings("PMD.SystemPrintln") + public T printDefaults() { + System.out.println(defaults.printConfiguration()); + return self(); + } + + /** + * @return client itself + */ + @SuppressWarnings("unchecked") + protected T self() { + return (T) this; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientRequestBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientRequestBuilder.java new file mode 100644 index 000000000..8bf1c28b2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientRequestBuilder.java @@ -0,0 +1,705 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.message.internal.TracingLogger; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.util.JerseyExceptionHandling; +import ru.vyarus.dropwizard.guice.test.client.builder.util.VoidBodyReader; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Request builder. Provides all the same configuration methods as jersey client builder. + * Supports default values, provided from the client. + *

        + * Request could be executed: + *

          + *
        • With direct response mapping: {@link #as(Class)}
        • + *
        • As void {@link #asVoid()} when response body is not important, just the success status
        • + *
        • With success validation {@link #expectSuccess(Integer...)} (it will also throw exception according to + * response status)
        • + *
        • With fail validation: {@link #expectFailure(Integer...)}
        • + *
        • With redirect validation: {@link #expectRedirect(Integer...)}
        • + *
        • Without status checks: {@link #invoke()}
        • + *
        + *

        + * In cases, when direct result mapping is not requested, a special response wrapper would be returned, supporting + * assertions in a builder manner. + *

        + * Builder does not hide jerse {@link jakarta.ws.rs.client.WebTarget} and + * {@link jakarta.ws.rs.client.Invocation.Builder} objects: they could be configured manually with: + * {@link #configurePath(Function)} and {@link #configureRequest(Consumer)}. + * + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +@SuppressWarnings("PMD.TooManyMethods") +public class TestClientRequestBuilder { + + private final WebTarget target; + private final String httpMethod; + private final Entity body; + private final TestRequestConfig config; + + /** + * Construct new request builder. + * + * @param target request target + * @param httpMethod request method + * @param body request entity (body) + * @param defaults default request configuration (common headers, cookies, path params, etc) + */ + public TestClientRequestBuilder(final WebTarget target, final String httpMethod, final Entity body, + final @Nullable TestRequestConfig defaults) { + this.target = target; + this.httpMethod = Preconditions.checkNotNull(httpMethod, "Http method required"); + this.body = body; + this.config = new TestRequestConfig(defaults); + } + + // --------------------------------------------------------------------------------- RESPONSE CONFIG + + /** + * Manual jersey target object configuration. + * + * @param modifier function, applying target configuration + * @return builder instance for chained calls + */ + public TestClientRequestBuilder configurePath(final Function modifier) { + this.config.configurePath(modifier); + return this; + } + + /** + * Manual jersey request object configuration. + * + * @param modifier consumer, applying request configuration + * @return builder instance for chained calls + */ + public TestClientRequestBuilder configureRequest(final Consumer modifier) { + this.config.configureRequest(modifier); + return this; + } + + /** + * Disable redirects following to validate redirection correctness (or redirection fact). + *

        + * Option is enabled automatically when {@link #expectRedirect(Integer...)} is used. + * + * @return builder instance for chained calls + */ + public TestClientRequestBuilder notFollowRedirects() { + this.config.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE); + return this; + } + + /** + * In theory, jersey should avoid body mapping when void response requested + * ({@code builder.readEntity(Void.class)}), but jersey tries to map it and fils. + * This shortcut registers a special body mapper which would completely ignore the response body. + *

        + * Enabled automatically when {@link #asVoid()} is used. + * + * @return builder instance for chained calls + */ + public TestClientRequestBuilder noBodyMappingForVoid() { + this.config.register(VoidBodyReader.class); + return this; + } + + /** + * Configure request Accept header ( + * {@link jakarta.ws.rs.client.Invocation.Builder#accept(jakarta.ws.rs.core.MediaType...)}). + *

        + * Multiple calls override previous value. + * + * @param accept media types required for response + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.MediaType + */ + public TestClientRequestBuilder accept(final MediaType... accept) { + final String[] res = Arrays.stream(accept).map(mediaType -> + RuntimeDelegate.getInstance().createHeaderDelegate(MediaType.class).toString(mediaType)) + .toArray(String[]::new); + return accept(res); + } + + /** + * Configure request Accept header ({@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}). + *

        + * Multiple calls override previous value. + * + * @param accept media types required for response + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.MediaType + */ + public TestClientRequestBuilder accept(final String... accept) { + this.config.accept(accept); + return this; + } + + /** + * Configure request Accept-Language header + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(java.util.Locale...)}). + *

        + * Multiple calls override previous value. + * + * @param language languages to accept + * @return builder instance for chained calls + */ + public TestClientRequestBuilder acceptLanguage(final Locale... language) { + final String[] res = Arrays.stream(language).map(Locale::toString).toArray(String[]::new); + this.config.acceptLanguage(res); + return this; + } + + /** + * Configure request Accept-Language header + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}). + *

        + * Multiple calls override previous value. + * + * @param language languages to accept + * @return builder instance for chained calls + */ + public TestClientRequestBuilder acceptLanguage(final String... language) { + this.config.acceptLanguage(language); + return this; + } + + /** + * Configure request Accept-Encoding header + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptEncoding(String...)}). + *

        + * Multiple calls override previous value. + * + * @param encodings encodings to accept + * @return builder instance for chained calls + */ + public TestClientRequestBuilder acceptEncoding(final String... encodings) { + this.config.acceptEncoding(encodings); + return this; + } + + /** + * Configure request query parameter ({@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}). + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name query parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestClientRequestBuilder queryParam(final String name, final Object value) { + this.config.queryParam(name, value); + return this; + } + + /** + * Configure multiple query params at once. + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls append to previous configurations (parameters with the same name are overridden) + * + * @param params query params map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder queryParams(final Map params) { + for (Map.Entry entry : params.entrySet()) { + this.config.queryParam(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Configure request matrix parameter ({@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}). + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name matrix parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestClientRequestBuilder matrixParam(final String name, final Object value) { + this.config.matrixParam(name, value); + return this; + } + + /** + * Configure multiple matrix params at once. + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls append to previous configurations (parameters with the same name are overridden) + * + * @param params query params map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder matrixParams(final Map params) { + for (Map.Entry entry : params.entrySet()) { + this.config.matrixParam(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Configure request path parameter + * ({@link jakarta.ws.rs.client.WebTarget#resolveTemplateFromEncoded(String, Object)}). + *

        + * Note: parameter value assumed to be encoded to be able to specify matrix parameters (in the middle of the path): + * {@code pathParam("var", "/path;var=1;val=2")}. + * + * @param name parameter name + * @param value parameter value + * @return builder instance for chained calls + */ + public TestClientRequestBuilder pathParam(final String name, final Object value) { + this.config.pathParam(name, value); + return this; + } + + /** + * Configure multiple path params at once. + *

        + * Multiple calls append to previous configurations (parameters with the same name are overridden) + * + * @param params parameters map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder pathParams(final Map params) { + for (Map.Entry entry : params.entrySet()) { + this.config.pathParam(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Configure request header ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param value header value + * @return builder instance for chained calls + */ + public TestClientRequestBuilder header(final HttpHeader name, final String value) { + return header(name.toString(), value); + } + + /** + * Configure request header ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param value header value + * @return builder instance for chained calls + */ + public TestClientRequestBuilder header(final String name, final String value) { + this.config.header(name, value); + return this; + } + + /** + * Configure multiple headers at once. + *

        + * Multiple calls append to previous configurations (headers with the same name are overridden) + * + * @param headers headers map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder headers(final Map headers) { + for (Map.Entry entry : headers.entrySet()) { + this.config.header(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Configure request cookie ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(String, String)}. + *

        + * Multiple calls override previous value. + * + * @param name cookie name + * @param value cookie value + * @return builder instance for chained calls + */ + public TestClientRequestBuilder cookie(final String name, final String value) { + return cookie(new NewCookie.Builder(name) + .value(value) + .build()); + } + + /** + * Configure request cookie ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Use cookie builder: {@code new NewCookie.Builder(name).value(value).build()}. + *

        + * Multiple calls override previous value. + * + * @param cookie cookie value + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public TestClientRequestBuilder cookie(final Cookie cookie) { + this.config.cookie(cookie); + return this; + } + + /** + * Configure multiple cookies at once. + *

        + * Multiple calls append to previous configurations (cookies with the same name are overridden). + * + * @param cookies cookie map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder cookies(final Map cookies) { + for (Map.Entry entry : cookies.entrySet()) { + cookie(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Jersey client property configuration ({@link org.glassfish.jersey.client.JerseyClient#property(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name property name + * @param value property value + * @return builder instance for chained calls + * @see org.glassfish.jersey.client.ClientProperties + */ + public TestClientRequestBuilder property(final String name, final Object value) { + this.config.property(name, value); + return this; + } + + /** + * Configure multiple properties at once. + *

        + * Multiple calls append to previous configurations (properties with the same name are overridden). + * + * @param props properties map + * @return builder instance for chained calls + */ + public TestClientRequestBuilder properties(final Map props) { + for (Map.Entry entry : props.entrySet()) { + this.config.property(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Jersey client extension registration ({@link org.glassfish.jersey.client.JerseyClient#register(Class)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader}. + *

        + * Multiple registrations of the same class will be ignored (extensions tracked by type). + * + * @param type extension class + * @return builder instance for chained calls + */ + public TestClientRequestBuilder register(final Class type) { + this.config.register(type); + return this; + } + + /** + * Jersey client extension registration ({@link org.glassfish.jersey.client.JerseyClient#register(Object)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader} (as instance). + *

        + * When called with different instances of the same class: only the latest registration will be used + * (extensions tracked by type). + * + * @param extension extensions instance + * @return builder instance for chained calls + */ + public TestClientRequestBuilder register(final Object extension) { + this.config.register(extension); + return this; + } + + /** + * Configure request Cache-Control header + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control header value (string) to apply to request. + * @return builder instance for chained calls + */ + public TestClientRequestBuilder cacheControl(final String cacheControl) { + this.config.cacheControl(cacheControl); + return this; + } + + /** + * Configure request Cache-Control header + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control settings to apply to request. + * @return builder instance for chained calls + */ + public TestClientRequestBuilder cacheControl(final CacheControl cacheControl) { + this.config.cacheControl(cacheControl); + return this; + } + + /** + * Enable console reporting for configured default values and applied request customizations. + * + * @return builder instance for chained calls + */ + public TestClientRequestBuilder debug() { + this.config.debug(true); + return this; + } + + /** + * Shortcut for jersey trace requesting: jersey should return trace as X-Jersey-Tracing-NNN headers. + *

        + * WARNING: Tracing support MUST be + * enabled on server with: + * {@code environment.jersey().property(ServerProperties.TRACING, TracingConfig.ON_DEMAND.name());} + *

        + * This is assumed to be used for remote apis. + * + * @return builder instance for chained calls + */ + public TestClientRequestBuilder enableJerseyTrace() { + return enableJerseyTrace(TracingLogger.Level.SUMMARY); + } + + /** + * {@link #enableJerseyTrace()} with custom trace level. + * + * @param level trace level + * @return builder instance for chained calls + */ + public TestClientRequestBuilder enableJerseyTrace(final TracingLogger.Level level) { + header(TracingLogger.HEADER_ACCEPT, "true"); + header(TracingLogger.HEADER_THRESHOLD, level.name()); + return this; + } + + /** + * Assert {@link jakarta.ws.rs.client.WebTarget} and {@link jakarta.ws.rs.client.Invocation.Builder} + * objects configurations. Could be used to verify request correctness. + *

        + * Implicitly enables {@link #debug()}, so all applied configurations would be printed to console first and + * then assertions applied. + *

        + * Assertions executed just before request execution. + * + * @param action assertion action + * @return builder instance for chanined calls + */ + public TestClientRequestBuilder assertRequest(final Consumer action) { + this.config.assertRequest(action); + return this; + } + + // --------------------------------------------------------------------------------- THE CALL + + /** + * Invoke request and expect successful status (2xx). Response body is ignored. + *

        + * In case of not successful request, exception would be thrown according to response status. + *

        + * Note: this option would implicitly enable {@link #noBodyMappingForVoid()}. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + */ + @SuppressWarnings("PMD.LinguisticNaming") + public void asVoid() { + as(Void.class); + } + + /** + * Invoke request and expect successful status (2xx). Response body is mapped to the specified type. + * This is essentially the same as jersey shortcut methods like {@code response.get(SomeEntity.class)}. + *

        + * In case of not successful request, exception would be thrown according to response status. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + * + * @param type response type + * @param response type + * @return mapped response instance + */ + public T as(final @Nullable Class type) { + return as(type == null ? new GenericType<>(Void.class) : new GenericType<>(type)); + } + + /** + * Invoke request and expect successful status (2xx). Response body is mapped to the specified type. + * This is essentially the same as jersey shortcut methods like + * {@code response.get(new GenericType(){}}. + *

        + * In case of not successful request, exception would be thrown according to response status. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + * + * @param type response type + * @param response type + * @return mapped response instance + */ + public T as(final @Nullable GenericType type) { + final GenericType resType = type == null ? new GenericType<>(Void.class) : type; + if (resType.getRawType().equals(Void.class)) { + // avoid errors on void mapping + noBodyMappingForVoid(); + } + final Invocation.Builder request = this.config.applyRequestConfiguration(this.target); + // important: api with the result type check for throwing error on not successful response + if (body == null) { + return request.method(httpMethod, resType); + } else { + return request.method(httpMethod, body, resType); + } + } + + /** + * Shortcut to {@link #as(Class)}. + * + * @return response body as string + */ + public String asString() { + return as(String.class); + } + + /** + * Invoke request without status validation (same as jersey + * {@link jakarta.ws.rs.client.Invocation.Builder#invoke()}). + * For automatic status validation see {@link #expectSuccess(Integer...)}, {@link #expectFailure(Integer...)} + * and {@link #expectRedirect(Integer...)}. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + *

        + * Note: IDE may highlight that method must be used under 'try-with-resources` (because the underlying response + * must be correctly closed), but you can ignore it, because all responses are closed automatically. + * + * @return response wrapper object with additional assertions + */ + public TestClientResponse invoke() { + final Invocation.Builder request = this.config.applyRequestConfiguration(this.target); + final Response response; + if (body == null) { + response = request.method(httpMethod); + } else { + response = request.method(httpMethod, body); + } + return new TestClientResponse(response); + } + + /** + * Invoke a request with status validation. It will throw an exception for non 2xx response + * (same as {@link #as(Class)}). Will throw assertion error if response status does not match. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + *

        + * Note: IDE may highlight that method must be used under 'try-with-resources` (because the underlying response + * must be correctly closed), but you can ignore it, because all responses are closed automatically. + * + * @param statuses (optional) statuses to match (when exact status validation is important) + * @return response wrapper object with additional assertions + */ + public TestClientResponse expectSuccess(final Integer... statuses) { + final TestClientResponse response = invoke(); + // unify behavior with jersey auto conversion case (so jersey would throw the same exceptions + // in case of not successful response) + JerseyExceptionHandling.throwIfNotSuccess(response.asResponse()); + return statuses.length > 0 ? response.assertStatus(statuses) : response; + } + + /** + * Invoke a request with status validation. Will throw assertion error if response status is success or + * does not match one of the provided statuses. + *

        + * Suitable for testing error response (or when exception mapped by some exception mapper). + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + *

        + * Note: IDE may highlight that method must be used under 'try-with-resources` (because the underlying response + * must be correctly closed), but you can ignore it, because all responses are closed automatically. + * + * @param statuses (optional) statuses to match (when exact status validation is important) + * @return response wrapper object with additional assertions + */ + public TestClientResponse expectFailure(final Integer... statuses) { + final TestClientResponse response = invoke().assertFail(); + return statuses.length > 0 ? response.assertStatus(statuses) : response; + } + + /** + * Invoke a request with status validation. Will throw assertion error if response status is not redirect or + * does not match one of provided statuses. + *

        + * Note: implicitly enables {@link #notFollowRedirects()}. + *

        + * Under {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} it will bypass original exception + * (when no exception mappers registered). + *

        + * Note: IDE may highlight that method must be used under 'try-with-resources` (because the underlying response + * must be correctly closed), but you can ignore it, because all responses are closed automatically. + * + * @param statuses (optional) statuses to match (when exact status validation is important) + * @return response wrapper object with additional assertions + */ + public TestClientResponse expectRedirect(final Integer... statuses) { + // disable auto redirects + notFollowRedirects(); + final TestClientResponse response = invoke().assertRedirect(); + return statuses.length > 0 ? response.assertStatus(statuses) : response; + } + + /** + * @return request configuration + */ + public TestRequestConfig getConfig() { + return config; + } + + @Override + public String toString() { + return "Request builder: " + httpMethod + " " + target.getUri().toString(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientResponse.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientResponse.java new file mode 100644 index 000000000..1cc9c3bce --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestClientResponse.java @@ -0,0 +1,584 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import org.glassfish.jersey.message.internal.CacheControlProvider; +import org.junit.jupiter.api.Assertions; +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; +import ru.vyarus.dropwizard.guice.test.client.util.FileDownloadUtil; +import ru.vyarus.dropwizard.guice.test.client.builder.util.TestClientResponseCleanup; +import ru.vyarus.java.generics.resolver.context.container.ParameterizedTypeImpl; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Wrapper for jersey {@link jakarta.ws.rs.core.Response} with extra assertion shortcuts (jersey response object + * is general-purpose and this api assumed to be used for tests). + *

        + * Provides the following method groups: + *

          + *
        • "as*" methods convert the response body (shortcuts for + * {@link jakarta.ws.rs.core.Response#readEntity(Class)})
        • + *
        • "assert*" methods to simplify response assertions. All these methods use junit 5 assertions (methods with + * {@link java.util.function.Predicate} also use assertions internally).
        • + *
        • "with*" methods for manual operations with various objects (to not create additional variable in test)
        • + *
        + *

        + * {@link jakarta.ws.rs.core.Response} would close only if response body was read. To indicate the importance of + * manual close, {@link java.lang.AutoCloseable} is implemented (same as in response) and so IDEA would + * highlight usage without "try-with-resources". You could ignore it when the client is used with guicey test + * extensions, as all responses would be closed just after the test application shutdown. + *

        + * Original response object is accessible with {@link #asResponse()}. + * + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.TooManyMethods", "checkstyle:MultipleStringLiterals"}) +public class TestClientResponse implements AutoCloseable { + + private static final String RESPONSE_DOES_NOT_MATCH = "Response does not match condition"; + private final Response response; + + /** + * Create a client response wrapper. + * + * @param response response object to wrap + */ + public TestClientResponse(final Response response) { + this.response = response; + // response might be not closed (if the body is not read) keep tracking response until the application shutdown + registerResponse(response); + } + + /** + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @return response body as string + */ + public String asString() { + return response.readEntity(String.class); + } + + /** + * Shortcut method to avoid {@link jakarta.ws.rs.core.GenericType} usage for simple lists (same as + * {@code response.readEntity(new GenericType<List<EntityType>>(){})}). + *

        + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @param entityType list entities type + * @param entity type + * @return response body, read as list + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + */ + public List asList(final Class entityType) { + return response.readEntity(new GenericType<>(new ParameterizedTypeImpl(List.class, entityType))); + } + + /** + * Read response body as declared type (same as {@link jakarta.ws.rs.core.Response#readEntity(Class)}). + * For complex types use {@link #as(GenericType)}. + *

        + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @param entityType entity type + * @param entity type + * @return result mapped into entity + * @throws java.lang.IllegalStateException if entity was already read + */ + public T as(final Class entityType) { + return response.readEntity(entityType); + } + + /** + * Required for types with generics.For example: {@code .as(new GenericType>(){})}. + *

        + * Note: when the result is assigned to variable, there is no need to specify type - use diamond operator + * ({@code new GenericType<>(){}}) (but there is no way to avoid specifying generic type at all). + *

        + * For simple list cases use {@link #asList(Class)} + *

        + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @param entityType entity type + * @param entity type + * @return result mapped into entity + * @throws java.lang.IllegalStateException if entity was already read + */ + public T as(final GenericType entityType) { + return response.readEntity(entityType); + } + + /** + * General response conversion logic. Exists for chained calls - to avoid redundant variable usage in the test. + *

        + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @param converter response converter. + * @param entity type + * @return conversion result + */ + public T as(final Function converter) { + return converter.apply(response); + } + + /** + * @return raw response object + */ + public Response asResponse() { + return response; + } + + /** + * Download the response file into a directory. In contrast to {@code response.readEntity(File.class)}, this + * method preserves file name (if provided) and creates not temporary file. If file name collide with already + * existing file, created file name would be modified with "(index)", + *

        + * Usually it's easier to manage local temp directory in test rather than rely on global temp. + *

        + * Warning: there is no explicit check for the request success state because this method might be used to read + * the error body. Use {@link #assertSuccess()} explicitly to make sure request was successful. + * + * @param tmpDir temporary files directory + * @return downloaded file path + */ + public Path asFile(final Path tmpDir) { + return FileDownloadUtil.download(asResponse(), tmpDir); + } + + /** + * Close the underlying response object. This is required ONLY when the response body was not acquired by any of + * as* methods (excluding asResponse). + *

        + * It is not required to call it explicitly when client is used with guicey test extensions because guicey would + * close all used responses after test application shutdown. + */ + @Override + public void close() { + response.close(); + } + + + // ------------------------------------------------------------ ASSERTIONS + + /** + * Assert a mapped response with custom condition. + *

        + * For custom assertions use {@link #withResponse(Class, java.util.function.Consumer)}. + * + * @param entityType entity type + * @param predicate assertion condition + * @param entity type + * @return response instance for chained calls + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + */ + public TestClientResponse assertResponse(final Class entityType, final Predicate predicate) { + Assertions.assertTrue(predicate.test(as(entityType)), RESPONSE_DOES_NOT_MATCH); + return this; + } + + /** + * Assert a mapped response with custom condition. + *

        + * For custom assertions use {@link #withResponse(jakarta.ws.rs.core.GenericType, java.util.function.Consumer)}. + * + * @param entityType entity type + * @param predicate assertion condition + * @param entity type + * @return response instance for chained calls + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + */ + public TestClientResponse assertResponse(final GenericType entityType, final Predicate predicate) { + Assertions.assertTrue(predicate.test(as(entityType)), RESPONSE_DOES_NOT_MATCH); + return this; + } + + /** + * Assert a response object (general assertion logic). + *

        + * For custom assertions use {@link #withResponse(java.util.function.Consumer)}. + * + * @param consumer response assertion + * @return response instance for chained calls + */ + public TestClientResponse assertResponse(final Predicate consumer) { + return withResponse(response -> Assertions + .assertTrue(consumer.test(response), RESPONSE_DOES_NOT_MATCH)); + } + + /** + * Assert response status to be one of the provided statuses. Note that often it's simpler to check for status + * family using {@link #assertStatus(jakarta.ws.rs.core.Response.Status.Family)}. + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @param expectedStatuses expected statuses + * @return response instance for chained calls + * @see org.apache.hc.core5.http.HttpStatus + */ + public TestClientResponse assertStatus(final Integer... expectedStatuses) { + Preconditions.checkArgument(expectedStatuses.length > 0, "At least one status is required"); + Assertions.assertTrue(Arrays.asList(expectedStatuses).contains(response.getStatus()), + () -> "Unexpected response status " + response.getStatus() + + " when expected " + Arrays.stream(expectedStatuses) + .map(Object::toString) + .collect(Collectors.joining(" or "))); + return this; + } + + /** + * Assert status family (family is a first digit in status code so SUCCESS family would mean any of 200, 201, + * 204, etc.). + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @param family expected family + * @return response instance for chained calls + */ + public TestClientResponse assertStatus(final Response.Status.Family family) { + return withStatus(statusType -> Assertions + .assertEquals(family, statusType.getFamily(), "Expected '" + family + + "' response status, but found '" + statusType.getFamily() + "'")); + } + + /** + * Assert status object. + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @param predicate predicate to check status object + * @return response instance for chained calls + */ + public TestClientResponse assertStatus(final Predicate predicate) { + return withStatus(statusType -> Assertions + .assertTrue(predicate.test(statusType), "Response status '" + statusType.getStatusCode() + " " + + statusType.getReasonPhrase() + "' does not match condition")); + } + + /** + * Assert response status to be 2xx. + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @return response instance for chained calls + */ + public TestClientResponse assertSuccess() { + // all 2xx + return assertStatus(Response.Status.Family.SUCCESSFUL); + } + + /** + * Assert response status is not 2xx (not success). Note that redirection also falls into this category, + * but, as normally, the client follows redirects, then it should not be a problem. + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @return response instance for chained calls + */ + public TestClientResponse assertFail() { + return withStatus(statusType -> + Assertions.assertNotEquals(Response.Status.Family.SUCCESSFUL, statusType.getFamily(), + "Failed response expected, but found '" + statusType.getFamily() + "'")); + } + + /** + * Assert response status is 3xx. Note that by default the client follows redirects, so it's important to disable + * it to assert redirection correctness. + *

        + * For custom assertions use {@link #withStatus(java.util.function.Consumer)}. + * + * @return response instance for chained calls + */ + public TestClientResponse assertRedirect() { + return assertStatus(Response.Status.Family.REDIRECTION); + } + + /** + * Assert cache control header value. + *

        + * For custom assertions use {@link #withCacheControl(java.util.function.Consumer)}. + * + * @param predicate assertion condition + * @return response instance for chained calls + */ + public TestClientResponse assertCacheControl(final Predicate predicate) { + return assertHeader(HttpHeader.CACHE_CONTROL, s -> predicate + .test(new CacheControlProvider().fromString(s))); + } + + /** + * Assert the response have no body. + *

        + * For custom assertions use {@link #withResponse(java.util.function.Consumer)} + * + * @return response instance for chained calls + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + */ + public TestClientResponse assertVoidResponse() { + Assertions.assertFalse(response.hasEntity(), "Void response expected, but found: \n" + asString()); + return this; + } + + /** + * Assert response media type. + * + * @param mediaType expected media type + * @return response instance for chained calls + */ + public TestClientResponse assertMedia(final MediaType mediaType) { + Assertions.assertEquals(mediaType, response.getMediaType(), "Expected '" + mediaType + + "' media type, but found '" + response.getMediaType() + "'"); + return this; + } + + /** + * Assert response locale. + * + * @param locale expected locale + * @return response instance for chained calls + */ + public TestClientResponse assertLocale(final Locale locale) { + Assertions.assertEquals(locale, response.getLanguage(), "Expected '" + locale + + "' response locale, but found '" + response.getLanguage() + "'"); + return this; + } + + /** + * Assert header value. In case of multiple headers present, its values would be concatenated with "," and the + * resulting string would be compared with the expected value. + *

        + * For custom assertions use {@link #withHeader(org.eclipse.jetty.http.HttpHeader, java.util.function.Consumer)}. + * + * @param name header name + * @param value expected value + * @return response instance for chained calls + */ + public TestClientResponse assertHeader(final String name, final String value) { + return withHeader(name, s -> Assertions.assertEquals(value, s, + "Expected header '" + name + "' value '" + value + "', but found '" + s + "'")); + } + + /** + * Assert header value with predicate. In case of multiple headers present, its values would be concatenated + * with "," and the resulting string would be compared with the expected value. + *

        + * For custom assertions use {@link #withHeader(org.eclipse.jetty.http.HttpHeader, java.util.function.Consumer)}. + * + * @param name header name + * @param predicate value predicate + * @return response instance for chained calls + */ + public TestClientResponse assertHeader(final String name, final Predicate predicate) { + return withHeader(name, s -> Assertions.assertTrue(predicate.test(s), + "Header '" + name + ": " + s + "' does not match condition") + ); + } + + /** + * Assert header value. In case of multiple headers present, its values would be concatenated with "," and the + * resulting string would be compared with the expected value. + *

        + * For custom assertions use {@link #withHeader(org.eclipse.jetty.http.HttpHeader, java.util.function.Consumer)}. + * + * @param header header name + * @param value expected value + * @return response instance for chained calls + */ + public TestClientResponse assertHeader(final HttpHeader header, final String value) { + return assertHeader(header.toString(), value); + } + + /** + * Assert header value with predicate. In case of multiple headers present, its values would be concatenated + * with "," and the resulting string would be provided to the predicate for evaluation. + *

        + * For custom assertions use {@link #withHeader(org.eclipse.jetty.http.HttpHeader, java.util.function.Consumer)}. + * + * @param predicate value predicate + * @param header header name + * @return response instance for chained calls + */ + public TestClientResponse assertHeader(final HttpHeader header, final Predicate predicate) { + return assertHeader(header.toString(), predicate); + } + + /** + * Assert cookie value. + *

        + * For custom assertions use {@link #withCookie(String, java.util.function.Consumer)}. + * + * @param name cookie name + * @param value expected value + * @return response instance for chained calls + */ + public TestClientResponse assertCookie(final String name, final String value) { + return withCookie(name, cookie -> Assertions.assertEquals(value, cookie.getValue(), + "Expected cookie '" + name + ": " + value + "', but found '" + cookie.getValue() + "'")); + } + + /** + * Assert cookie value with a predicate. + *

        + * For custom assertions use {@link #withCookie(String, java.util.function.Consumer)}. + * + * @param name cookie name + * @param predicate value predicate + * @return response instance for chained calls + */ + public TestClientResponse assertCookie(final String name, final Predicate predicate) { + return withCookie(name, cookie -> Assertions + .assertTrue(predicate.test(cookie), "Cookie '" + RuntimeDelegate.getInstance() + .createHeaderDelegate(Cookie.class).toString(cookie) + "' does not match condition")); + } + + // ------------------------------------------------------------ MANUAL CHECKS + + /** + * Assert response body mapping and the resulted object. + * + * @param entityType entity type + * @param consumer mapped result consumer + * @return response instance for chained calls + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + * @param entity type + */ + public TestClientResponse withResponse(final Class entityType, final Consumer consumer) { + consumer.accept(as(entityType)); + return this; + } + + /** + * Assert response body mapping and the resulted object. + * + * @param entityType entity type + * @param consumer mapped result consumer + * @return response instance for chained calls + * @throws java.lang.IllegalStateException if entity was already read (e.g. by {@link #as(Class)}) + * @param entity type + */ + public TestClientResponse withResponse(final GenericType entityType, final Consumer consumer) { + consumer.accept(as(entityType)); + return this; + } + + /** + * Manually examine a response object and perform manual assertions. + * + * @param consumer response object consumer + * @return response instance for chained calls + */ + public TestClientResponse withResponse(final Consumer consumer) { + consumer.accept(response); + return this; + } + + /** + * Manually examine a status object and perform assertions. + * + * @param consumer status object consumer + * @return response instance for chained calls + */ + public TestClientResponse withStatus(final Consumer consumer) { + consumer.accept(response.getStatusInfo()); + return this; + } + + /** + * Manually examine header and perform assertions. In case of multiple headers present, its values would be + * concatenated with "," and the resulting string would be provided for manual assertions. + * + * @param header header name + * @param consumer value consumer + * @return response instance for chained calls + */ + public TestClientResponse withHeader(final HttpHeader header, final Consumer consumer) { + return withHeader(header.toString(), consumer); + } + + /** + * Manually examine header and perform assertions. In case of multiple headers present, its values would be + * concatenated with "," and the resulting string would be provided for manual assertions. + * + * @param name header name + * @param consumer value consumer + * @return response instance for chained calls + */ + public TestClientResponse withHeader(final String name, final Consumer consumer) { + if (response.getHeaders().containsKey(name)) { + consumer.accept(response.getHeaderString(name)); + } else { + final String headers = response.getHeaders().keySet().stream() + .map(key -> key + "=" + response.getHeaderString(key)) + .collect(Collectors.joining("\n")); + Assertions.fail("Missing header '" + name + "' in response. Available headers: \n" + headers); + } + return this; + } + + /** + * Manually examine cookie and perform assertions. + * + * @param name cookie name + * @param consumer cookie object consumer + * @return response instance for chained calls + */ + public TestClientResponse withCookie(final String name, final Consumer consumer) { + if (response.getCookies().containsKey(name)) { + consumer.accept(response.getCookies().get(name)); + } else { + final String cookies = response.getCookies().values().stream() + .map(key -> key.getName() + "=" + key.getValue()) + .collect(Collectors.joining("\n")); + Assertions.fail("Missing cookie '" + name + "' in response. Available cookies: \n" + + cookies); + } + return this; + } + + /** + * Manually examine the cache control header and perform assertions. + * + * @param consumer cache control consumer + * @return response instance for chained calls + */ + public TestClientResponse withCacheControl(final Consumer consumer) { + return withHeader(HttpHeader.CACHE_CONTROL, s -> consumer + .accept(new CacheControlProvider().fromString(s))); + } + + @Override + public String toString() { + return "Response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase(); + } + + private static void registerResponse(final Response response) { + // Only when outer context is present (all junit extensions and most generic test cases) + // Using shared state to automate cleanup just after application shutdown. + // This isn't intended to be a 100% guarantee, just a help for some cases (that's why no errors) + if (TestSupportHolder.isContextSet()) { + SharedConfigurationState.lookupOrCreate(TestSupport.getContext().getEnvironment(), + TestClientResponseCleanup.class, TestClientResponseCleanup::new) + .add(response); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfig.java new file mode 100644 index 000000000..c7ac67a73 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfig.java @@ -0,0 +1,945 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.util.RequestModifierSource; +import ru.vyarus.dropwizard.guice.test.client.builder.util.TestRequestConfigPrinter; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.JerseyRequestConfigurer; +import ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue; + +import java.text.DateFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Request configuration. Used to configure client defaults and for exact request configuration. + * The main problem with the jersey client api is that it split into two phases: request target (including + * query params) and request itself. So we have to collect all required data separately to apply both phases + * at once (to provide simpler api). + *

        + * The configuration is hierarchical: sub-clients could inherit top level configurations (defaults copies on + * creation). + *

        + * Most properties provide {@link Supplier}-based configuration variant for lazy evaluation (applicable for defaults, + * when actual value is resolved in time of request creation and not in time of default registration). + *

        + * Also provides direct access for {@link jakarta.ws.rs.client.WebTarget} and + * {@link jakarta.ws.rs.client.Invocation.Builder} objects with {@link #configurePath(java.util.function.Function)} and + * {@link #configureRequest(java.util.function.Consumer)} methods. Config itself is registered in these methods + * to apply all collected data. + *

        + * Object also keeps data formatters to be used in form builder. + * + * @author Vyacheslav Rusakov + * @since 13.09.2025 + */ +@SuppressWarnings({"PMD.GodClass", "PMD.TooManyMethods", "PMD.ExcessivePublicCount", "PMD.CouplingBetweenObjects", + "PMD.CyclomaticComplexity", "checkstyle:OverloadMethodsDeclarationOrder"}) +public class TestRequestConfig implements Function, Consumer { + + // suppliers used for "defaults" case + private final Map> queryParams = new LinkedHashMap<>(); + private final Map> matrixParams = new LinkedHashMap<>(); + private final Map> pathParams = new LinkedHashMap<>(); + private final Map> headers = new LinkedHashMap<>(); + private final Map> cookies = new LinkedHashMap<>(); + private final Map> properties = new LinkedHashMap<>(); + // jersey extensions registration (normally registered with .register()) + private final Map, SourceAwareValue> extensions = new LinkedHashMap<>(); + + private final List>> pathModifiers = new ArrayList<>(); + private final List>> requestModifiers = new ArrayList<>(); + + private SourceAwareValue acceptHeader; + private SourceAwareValue languageHeader; + private SourceAwareValue encondingHeader; + private SourceAwareValue cache; + // used for form fields serialization + private SourceAwareValue dateFormatter; + private SourceAwareValue dateTimeFormatter; + private boolean debugEnabled; + private Consumer requestAssertion; + + /** + * Create a request config. + * + * @param base base configuration (optional) to copy values from + */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") + public TestRequestConfig(final @Nullable TestRequestConfig base) { + configurePath(this); + configureRequest(this); + if (base != null) { + copy(base); + } + } + + /** + * Provides direct access for jersey {@link jakarta.ws.rs.client.WebTarget} object configuration. + * + * @param modifier function, applying request target configuration + * @return builder instance for chained calls + */ + public TestRequestConfig configurePath(final Function modifier) { + pathModifiers.add(value(() -> modifier)); + return this; + } + + /** + * Provides direct access for jersey {@link jakarta.ws.rs.client.Invocation.Builder} object configuration. + * + * @param modifier consumer, applying request configuration + * @return builder instance for chained calls + */ + public TestRequestConfig configureRequest(final Consumer modifier) { + requestModifiers.add(value(() -> modifier)); + return this; + } + + /** + * Jersey client property configuration ({@link org.glassfish.jersey.client.JerseyClient#property(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name property name + * @param value property value + * @return builder instance for chained calls + * @see org.glassfish.jersey.client.ClientProperties + */ + public TestRequestConfig property(final String name, final Object value) { + return property(name, () -> value); + } + + /** + * Configure jersey client property configuration + * ({@link org.glassfish.jersey.client.JerseyClient#property(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name property name + * @param supplier value supplier + * @return builder instance for chained calls + * @see org.glassfish.jersey.client.ClientProperties + */ + public TestRequestConfig property(final String name, final Supplier supplier) { + properties.put(name, value(supplier)); + return this; + } + + /** + * Configure jersey client extension registration + * ({@link org.glassfish.jersey.client.JerseyClient#register(Class)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader}. + *

        + * Multiple registrations of the same class will be ignored (extensions tracked by type). + * + * @param type extension class + * @param extension type + * @return builder instance for chained calls + */ + @SuppressWarnings("unchecked") + public TestRequestConfig register(final Class type) { + return register(type, () -> (K) type); + } + + /** + * Configure jersey client extension registration + * ({@link org.glassfish.jersey.client.JerseyClient#register(Object)}). + * Could be useful, for example, to register custom {@link jakarta.ws.rs.ext.MessageBodyReader} (as instance). + *

        + * When called with different instances of the same class: only the latest registration will be used + * (extensions tracked by type). + * + * @param extension extensions instance + * @param extension type + * @return builder instance for chained calls + */ + @SuppressWarnings("unchecked") + public TestRequestConfig register(final K extension) { + return register((Class) extension.getClass(), () -> extension); + } + + /** + * Configure jersey client extension registration + * ({@link org.glassfish.jersey.client.JerseyClient#register(Object)}). + * Supplier could return either class or extension instance. + *

        + * When called with different instances of the same class: only the latest registration will be used + * (extensions tracked by type). + * + * @param type extension type + * @param supplier extension instance or null (for class) + * @param extension type + * @return builder instance for chained calls + */ + public TestRequestConfig register(final Class type, final @Nullable Supplier supplier) { + extensions.put(type, value(supplier)); + return this; + } + + /** + * Configure request Accept header ({@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}). + *

        + * Multiple calls override previous value. + * + * @param accept media types required for response + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.MediaType + */ + public TestRequestConfig accept(final String... accept) { + this.acceptHeader = value(() -> accept); + return this; + } + + /** + * Configure request Accept-Language header + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}). + *

        + * Multiple calls override previous value. + * + * @param language languages to accept + * @return builder instance for chained calls + */ + public TestRequestConfig acceptLanguage(final String... language) { + this.languageHeader = value(() -> language); + return this; + } + + /** + * Configure request Accept-Encoding header + * ({@link jakarta.ws.rs.client.Invocation.Builder#acceptEncoding(String...)}). + *

        + * Multiple calls override previous value. + * + * @param encoding encodings to accept + * @return builder instance for chained calls + */ + public TestRequestConfig acceptEncoding(final String... encoding) { + this.encondingHeader = value(() -> encoding); + return this; + } + + /** + * Configure request Cache-Control header + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control header value (as string). + * @return builder instance for chained calls + */ + public TestRequestConfig cacheControl(final String cacheControl) { + return cacheControl(RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString(cacheControl)); + } + + /** + * Configure request Cache-Control header + * ({@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(CacheControl)}). + *

        + * Multiple calls override previous value. + * + * @param cacheControl cache control settings to apply to request. + * @return builder instance for chained calls + */ + public TestRequestConfig cacheControl(final CacheControl cacheControl) { + this.cache = cacheControl == null ? null : value(() -> cacheControl); + return this; + } + + /** + * Configure request query parameter ({@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}). + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name query parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestRequestConfig queryParam(final String name, final Object value) { + return queryParam(name, () -> value); + } + + + /** + * Configure request query parameter ({@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}). + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name param name + * @param supplier value supplier (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestRequestConfig queryParam(final String name, final Supplier supplier) { + this.queryParams.put(name, value(supplier)); + return this; + } + + /** + * Configure request path parameter + * ({@link jakarta.ws.rs.client.WebTarget#resolveTemplateFromEncoded(String, Object)}). + *

        + * Note: parameter value assumed to be encoded to be able to specify matrix parameters (in the middle of the path): + * {@code pathParam("var", "/path;var=1;val=2")}. + * + * @param name parameter name + * @param value parameter value + * @return builder instance for chained calls + */ + public TestRequestConfig pathParam(final String name, final Object value) { + return pathParam(name, () -> value); + } + + /** + * Configure request path parameter + * ({@link jakarta.ws.rs.client.WebTarget#resolveTemplateFromEncoded(String, Object)}). + * + * @param name parameter name + * @param supplier parameter value supplier + * @return builder instance for chained calls + */ + public TestRequestConfig pathParam(final String name, final Supplier supplier) { + this.pathParams.put(name, value(supplier)); + return this; + } + + /** + * Configure request matrix parameter ({@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}). + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name matrix parameter name + * @param value parameter value (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestRequestConfig matrixParam(final String name, final Object value) { + return matrixParam(name, () -> value); + } + + + /** + * Configure request matrix parameter ({@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}). + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). The same result could be achieved by passing list or array into this api. + *

        + * Multiple calls with the same name override the previous value (the only way to specify multiple values is + * using list or array as value). + * + * @param name param name + * @param supplier value supplier (list or array for multiple values) + * @return builder instance for chained calls + */ + public TestRequestConfig matrixParam(final String name, final Supplier supplier) { + this.matrixParams.put(name, value(supplier)); + return this; + } + + /** + * Configure request header ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param value header value + * @return builder instance for chained calls + */ + public TestRequestConfig header(final String name, final Object value) { + return header(name, () -> value); + } + + /** + * Configure request header ({@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}). + *

        + * Multiple calls override previous value. + * + * @param name header name + * @param supplier value supplier + * @return builder instance for chained calls + */ + public TestRequestConfig header(final String name, final Supplier supplier) { + this.headers.put(name, value(supplier)); + return this; + } + + /** + * Configure request cookie ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Use cookie builder: {@code new NewCookie.Builder(name).value(value).build()}. + *

        + * Multiple calls override previous value. + * + * @param cookie cookie value + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public TestRequestConfig cookie(final Cookie cookie) { + return cookie(cookie.getName(), () -> cookie); + } + + /** + * Configure request cookie ({@link jakarta.ws.rs.client.Invocation.Builder#cookie(jakarta.ws.rs.core.Cookie)}. + *

        + * Multiple calls override previous value. + * + * @param name name + * @param supplier value suppler + * @return builder instance for chained calls + * @see jakarta.ws.rs.core.NewCookie + */ + public TestRequestConfig cookie(final String name, final Supplier supplier) { + this.cookies.put(name, value(supplier)); + return this; + } + + /** + * Configure java.util date formatter for form fields (used in + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder}). + * + * @param formatter date formatter + * @return builder instance for chained calls + */ + public TestRequestConfig formDateFormatter(final DateFormat formatter) { + this.dateFormatter = value(() -> formatter); + return this; + } + + /** + * Configure java.time formatter for form fields (used in + * {@link ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder}). + * + * @param formatter date formatter + * @return builder instance for chained calls + */ + public TestRequestConfig formDateTimeFormatter(final DateTimeFormatter formatter) { + this.dateTimeFormatter = value(() -> formatter); + return this; + } + + /** + * Enable debug output for configured requests. + * + * @param debug true to enable debug, false to disable + * @return builder instance for chained calls + */ + public TestRequestConfig debug(final boolean debug) { + this.debugEnabled = debug; + return this; + } + + /** + * Assert configuration applied to {@link jakarta.ws.rs.client.WebTarget} and + * {@link jakarta.ws.rs.client.Invocation.Builder}. + *

        + * Option implicitly enables debug. + *

        + * Assertions would be executed just before request execution + * + * @param assertion assertions + * @return builder instance for chained calls + */ + public TestRequestConfig assertRequest(final Consumer assertion) { + debug(true); + this.requestAssertion = assertion; + return this; + } + + /** + * Build jersey client request with configured values. + * + * @param webTarget target + * @return pre-configured jersey client request builder + */ + @SuppressWarnings("PMD.SystemPrintln") + public Invocation.Builder applyRequestConfiguration(final WebTarget webTarget) { + WebTarget target = webTarget; + if (debugEnabled) { + final RequestTracker tracker = new RequestTracker(); + target = tracker.track(webTarget, () -> { + System.out.println("Request configuration: \n" + printConfiguration()); + System.out.println("Jersey request configuration: \n" + tracker.getLog()); + if (requestAssertion != null) { + requestAssertion.accept(tracker); + } + }); + } + for (Supplier> fun : pathModifiers) { + target = fun.get().apply(target); + } + final Invocation.Builder request = target.request(); + requestModifiers.forEach(fun -> fun.get().accept(request)); + return request; + } + + /** + * Clear configuration. + * + * @return builder instance for chained calls + */ + public TestRequestConfig clear() { + queryParams.clear(); + matrixParams.clear(); + pathParams.clear(); + headers.clear(); + cookies.clear(); + properties.clear(); + extensions.clear(); + acceptHeader = null; + languageHeader = null; + encondingHeader = null; + pathModifiers.subList(1, pathModifiers.size()).clear(); + requestModifiers.subList(1, requestModifiers.size()).clear(); + dateFormatter = null; + dateTimeFormatter = null; + cache = null; + debugEnabled = false; + requestAssertion = null; + return this; + } + + /** + * @return true if any configuration applied, false otherwise + */ + @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:BooleanExpressionComplexity"}) + public boolean hasConfiguration() { + return !queryParams.isEmpty() + || !matrixParams.isEmpty() + || !pathParams.isEmpty() + || !headers.isEmpty() + || !cookies.isEmpty() + || !properties.isEmpty() + || !extensions.isEmpty() + || acceptHeader != null + || languageHeader != null + || encondingHeader != null + // config is a modifier itself + || pathModifiers.size() > 1 + || requestModifiers.size() > 1 + || dateFormatter != null + || dateTimeFormatter != null + || cache != null; + } + + /** + * @return render configured values as string + */ + public String printConfiguration() { + return TestRequestConfigPrinter.print(this); + } + + /** + * @return configured query parameter names or empty set if not configured + */ + public Set getConfiguredQueryParams() { + return queryParams.keySet(); + } + + /** + * @return configured query parameters map or empty map if not configured + */ + public Map getConfiguredQueryParamsMap() { + final Map res = new HashMap<>(); + queryParams.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return query parameters with declaration sources or empty map if not configured + */ + public Map> getConfiguredQueryParamsSource() { + return queryParams; + } + + /** + * @return configured matrix parameter names or empty set if not configured + */ + public Set getConfiguredMatrixParams() { + return matrixParams.keySet(); + } + + /** + * @return configured matrix parameters map or empty map if not configured + */ + public Map getConfiguredMatrixParamsMap() { + final Map res = new HashMap<>(); + matrixParams.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return matrix parameters with declaration sources or empty map if not configured + */ + public Map> getConfiguredMatrixParamsSource() { + return matrixParams; + } + + /** + * @return configured header names or empty set if not configured + */ + public Set getConfiguredHeaders() { + return headers.keySet(); + } + + /** + * @return configured headers map or empty map if not configured + */ + public Map getConfiguredHeadersMap() { + final Map res = new HashMap<>(); + headers.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return configured headers with declaration sources or empty map if not configured + */ + public Map> getConfiguredHeadersSource() { + return headers; + } + + /** + * @return configured cookie names or empty set if not configured + */ + public Set getConfiguredCookies() { + return cookies.keySet(); + } + + /** + * @return configured cookies map or empty map if not configured + */ + public Map getConfiguredCookiesMap() { + final Map res = new HashMap<>(); + cookies.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return configured cookie with declaration sources or empty map if not configured + */ + public Map> getConfiguredCookiesSource() { + return cookies; + } + + /** + * @return configured property names or empty set if not configured + */ + public Set getConfiguredProperties() { + return properties.keySet(); + } + + /** + * @return configured properties map or empty map if not configured + */ + public Map getConfiguredPropertiesMap() { + final Map res = new HashMap<>(); + properties.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return configured properties with declaration sources or empty map if not configured. + */ + public Map> getConfiguredPropertiesSource() { + return properties; + } + + /** + * @return configured extension classes or empty set if not configured. + */ + public Set> getConfiguredExtensions() { + return extensions.keySet(); + } + + /** + * Note: extensions registered with class will have class in value. + * + * @return configured extensions map or empty map if not configured + */ + public Map, Object> getConfiguredExtensionsMap() { + final Map, Object> res = new HashMap<>(); + extensions.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return configured extensions with declaration sources or empty map if not configured + */ + public Map, SourceAwareValue> getConfiguredExtensionsSource() { + return extensions; + } + + /** + * @return configured path param names or empty set if not configured + */ + public Set getConfiguredPathParams() { + return pathParams.keySet(); + } + + /** + * @return configured path params map or empty map if not configured + */ + public Map getConfiguredPathParamsMap() { + final Map res = new HashMap<>(); + pathParams.forEach((key, value) -> res.put(key, value.get())); + return res; + } + + /** + * @return configured path params with declaration sources or empty map if not configured + */ + public Map> getConfiguredPathParamsSource() { + return pathParams; + } + + /** + * @return configured accept media types or empty list if not configured + */ + public List getConfiguredAccepts() { + return acceptHeader == null ? Collections.emptyList() : Arrays.asList(acceptHeader.get()); + } + + /** + * @return configured media types with declaration source or null if not configured + */ + @Nullable + public SourceAwareValue getConfiguredAcceptsSource() { + return acceptHeader; + } + + /** + * @return configured languages or empty list if not configured + */ + public List getConfiguredLanguages() { + return languageHeader == null ? Collections.emptyList() : Arrays.asList(languageHeader.get()); + } + + /** + * @return configured languages with declaration source or null if not configured + */ + @Nullable + public SourceAwareValue getConfiguredLanguagesSource() { + return languageHeader; + } + + /** + * @return configured accept languages or empty list if not configured + */ + public List getConfiguredEncodings() { + return encondingHeader == null ? Collections.emptyList() : Arrays.asList(encondingHeader.get()); + } + + /** + * @return configured encodings with declaration source or null if not configured + */ + @Nullable + public SourceAwareValue getConfiguredEncodingsSource() { + return encondingHeader; + } + + /** + * @return configured java.util date formatter or null if not configured + */ + @Nullable + public DateFormat getConfiguredFormDateFormatter() { + return dateFormatter == null ? null : dateFormatter.get(); + } + + /** + * @return configured java.util date formatter supplier with declaration source or null if not configured + */ + @Nullable + public SourceAwareValue getConfiguredFormDateFormatterSource() { + return dateFormatter; + } + + /** + * @return configured java.time date formatter or null if not configured + */ + @Nullable + public DateTimeFormatter getConfiguredFormDateTimeFormatter() { + return dateTimeFormatter == null ? null : dateTimeFormatter.get(); + } + + /** + * @return configured java.time date formatter supplier with declaration source or null if not configured + */ + @Nullable + public SourceAwareValue getConfiguredFormDateTimeFormatterSource() { + return dateTimeFormatter; + } + + /** + * @return configured cache control or null if not configured + */ + @Nullable + public CacheControl getConfiguredCacheControl() { + return cache == null ? null : cache.get(); + } + + /** + * @return configured cache control with declaration source or null + */ + @Nullable + public SourceAwareValue getConfiguredCacheControlSource() { + return cache; + } + + /** + * @return custom path modifiers (excluding config object itself) + */ + public List> getConfiguredPathModifiers() { + return pathModifiers.size() == 1 ? Collections.emptyList() + : pathModifiers.subList(1, pathModifiers.size()) + .stream().map(SourceAwareValue::get) + .collect(Collectors.toList()); + } + + /** + * @return configured path modifiers with declaration sources or empty list + */ + public List>> getConfiguredPathModifiersSource() { + return pathModifiers.size() == 1 ? Collections.emptyList() + : pathModifiers.subList(1, pathModifiers.size()); + } + + /** + * @return custom request modifiers (excluding config object itself) + */ + public List> getConfiguredRequestModifiers() { + return requestModifiers.size() == 1 ? Collections.emptyList() + : requestModifiers.subList(1, requestModifiers.size()) + .stream().map(SourceAwareValue::get) + .collect(Collectors.toList()); + } + + /** + * @return configured request modifiers with declaration sources or empty list + */ + public List>> getConfiguredRequestModifiersSource() { + return requestModifiers.size() == 1 ? Collections.emptyList() + : requestModifiers.subList(1, requestModifiers.size()); + } + + /** + * @return true if debug enabled + */ + public boolean isDebugEnabled() { + return debugEnabled; + } + + /** + * Applies target configuration. Used under {@link #configurePath(java.util.function.Function)}. + *

        + * Implicitly called by {@link #applyRequestConfiguration(jakarta.ws.rs.client.WebTarget)}. + * + * @param webTarget target to configure + * @return configured target + */ + @Override + public WebTarget apply(final WebTarget webTarget) { + WebTarget target = webTarget; + if (!pathParams.isEmpty()) { + target = target.resolveTemplatesFromEncoded(getConfiguredPathParamsMap()); + } + if (!queryParams.isEmpty()) { + target = JerseyRequestConfigurer.applyQueryParams(target, queryParams); + } + if (!matrixParams.isEmpty()) { + target = JerseyRequestConfigurer.applyMatrixParams(target, matrixParams); + } + if (!properties.isEmpty()) { + for (Map.Entry> entry : properties.entrySet()) { + target.property(entry.getKey(), entry.getValue().get()); + } + } + if (!extensions.isEmpty()) { + JerseyRequestConfigurer.applyExtensions(target, extensions); + } + + return target; + } + + /** + * Applies builder configurations. Used under {@link #configureRequest(java.util.function.Consumer)}. + *

        + * Implicitly called by {@link #applyRequestConfiguration(jakarta.ws.rs.client.WebTarget)}. + * + * @param request builder to configure. + */ + @Override + public void accept(final Invocation.Builder request) { + if (acceptHeader != null) { + request.accept(acceptHeader.get()); + } + if (languageHeader != null) { + request.acceptLanguage(languageHeader.get()); + } + if (encondingHeader != null) { + request.acceptEncoding(encondingHeader.get()); + } + if (!headers.isEmpty()) { + headers.forEach((name, value) -> request.header(name, value.get())); + } + if (!cookies.isEmpty()) { + cookies.values().forEach(cookie -> request.cookie(cookie.get())); + } + if (cache != null) { + request.cacheControl(cache.get()); + } + } + + private void copy(final TestRequestConfig base) { + queryParams.putAll(base.queryParams); + matrixParams.putAll(base.matrixParams); + pathParams.putAll(base.pathParams); + headers.putAll(base.headers); + cookies.putAll(base.cookies); + properties.putAll(base.properties); + extensions.putAll(base.extensions); + acceptHeader = base.acceptHeader; + languageHeader = base.languageHeader; + encondingHeader = base.encondingHeader; + if (base.pathModifiers.size() > 1) { + pathModifiers.addAll(base.pathModifiers.subList(1, base.pathModifiers.size())); + } + if (base.requestModifiers.size() > 1) { + requestModifiers.addAll(base.requestModifiers.subList(1, base.requestModifiers.size())); + } + dateFormatter = base.dateFormatter; + dateTimeFormatter = base.dateTimeFormatter; + cache = base.cache; + debugEnabled = base.debugEnabled; + requestAssertion = base.requestAssertion; + } + + private SourceAwareValue value(final Supplier supplier) { + return new SourceAwareValue<>(supplier, RequestModifierSource.getSource()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelper.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelper.java new file mode 100644 index 000000000..7ddb42055 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelper.java @@ -0,0 +1,333 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.call; + +import com.google.common.base.Preconditions; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * Helper utilities to build multipart objects for rest method analysis api. + *

        + * For the most common case {@code post(@FormDataParam("file") InputStream stream, + * @FormDataParam("file") FormDataContentDisposition fileDetail)}: + *

        {@code .post(multipart.fromClasspath("/some.txt"),
        + *                   multipart.disposition("file", "some.txt"))}
        . + *

        + * it could be a single field: {@code post(@FormDataParam("file") FormDataBodyPart file)}: + *

        {@code .post(multipart.filePart("/some.txt"))}
        + * or to get file from classpath: + *
        {@code .post(multipart.streamPart("/some.txt"))}
        . + *

        + * There might be multiple files for the same field + * {@code post(@FormDataParam("file") List file)}: + *

        {@code .post(Arrays.asList(
        + *              multipart.filePart("/some.txt"),
        + *              multipart.filePart("/other.txt")))}
        . + *

        + * When method only accepts content-disposition mapping, it would be also used with en empty file content + * (as file content is not required, preserve available data): + * {@code post(@FormDataParam("file") FormDataContentDisposition file)} + *

        {@code .post(multipart.disposition("file", "some.txt"))}
        . + *

        + * The method parameter could be a complete multipart object (with all fields inside): + * {@code post(FormDataMultiPart multiPart)}: for such cases there is a special builder: + *

        {@code .post(multipart.multipart()
        + *                  .field("foo", "val")
        + *                  .file("file1", "/some.txt)
        + *                  .stream("file2", "/other.txt)
        + *                  .build())}
        + * + * @author Vyacheslav Rusakov + * @since 10.10.2025 + */ +public class MultipartArgumentHelper { + + /** + * Header would contain both utf-8 and ascii filenames. + * + * @param field field name + * @param filename file name + * @return content-disposition header + */ + public static String createDispositionHeader(final String field, final String filename) { + return "form-data; name=\"" + field + "\"; filename=\"" + + filename.replaceAll("[^\\p{ASCII}]", "") + "\"; filename*=UTF-8''" + + URLEncoder.encode(filename, StandardCharsets.UTF_8); + } + + /** + * Useful for rest methods declaring input stream and content disposition object. + * + * @param path classpath path + * @return input stream of classpath resource + * @throws java.lang.IllegalStateException if resource not found + */ + public InputStream fromClasspath(final String path) { + final InputStream res = MultipartArgumentHelper.class.getResourceAsStream(path); + Preconditions.checkState(res != null, "Classpath resource '%s' not found", path); + return res; + } + + /** + * Useful for rest methods declaring input stream and content disposition object. + * + * @param path local file path (relative to work dir) + * @return stream from local file + * @throws java.lang.IllegalStateException if file does not exist + * @see #fromClasspath(String) for classpath resource + * @see #file(String) for obtaining local file + */ + public InputStream fromFile(final String path) { + try { + return Files.newInputStream(file(path).toPath()); + } catch (Exception e) { + throw new IllegalStateException("Failed to read file '" + path + "' stream", e); + } + } + + /** + * @param path local file path (relative to work dir) + * @return file object + * @throws java.lang.IllegalStateException if file does not exist or it is a directory + */ + public File file(final String path) { + final File res = new File(path); + Preconditions.checkState(res.exists() && !res.isDirectory(), "'%s' does not exist or is a directory", path); + return res; + } + + /** + * Create content-disposition object. Useful for resource methods with input stream and disposition object. + * + * @param field field name + * @param file file to get filename from (may not exist) + * @return content-disposition object + */ + public FormDataContentDisposition disposition(final String field, final File file) { + return disposition(field, file.getName()); + } + + /** + * Create content-disposition object. Useful for resource methods with input stream and disposition object. + * File name is applied as utf-8 and ascii. + * + * @param field field name + * @param filename file name + * @return content-disposition object + */ + public FormDataContentDisposition disposition(final String field, final String filename) { + try { + return new FormDataContentDisposition(createDispositionHeader(field, filename)); + } catch (Exception e) { + throw new IllegalStateException("Failed to create content-disposition for field " + field, e); + } + } + + /** + * Create simple data part. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} could be declared + * as resource method parameter. + * + * @param field field name + * @param value field value + * @return simple body part + */ + public FormDataBodyPart part(final String field, final String value) { + return new FormDataBodyPart(field, value); + } + + /** + * Create file data part. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} could be declared + * as resource method parameter (but file part can't!). + * + * @param field field name + * @param file file object + * @return file body part + */ + public FileDataBodyPart filePart(final String field, final File file) { + return new FileDataBodyPart(field, file); + } + + /** + * Create file data part from local file path. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} + * could be declared as resource method parameter (but file part can't!). + * + * @param field field name + * @param path local file path (relative to work dir) + * @return file body part + * @throws java.lang.IllegalStateException if target file does not exist, or it is a directory + */ + public FileDataBodyPart filePart(final String field, final String path) { + return new FileDataBodyPart(field, file(path)); + } + + /** + * Create stream data part. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} could be declared as + * resource method parameter (but stream part can't!). + *

        + * Important: filename would be missed in this case! + * + * @param field field name + * @param stream stream + * @return stream body part + */ + public StreamDataBodyPart streamPart(final String field, final InputStream stream) { + return streamPart(field, stream, null); + } + + /** + * Create stream data part. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} could be declared as + * resource method parameter (but stream part can't!). + * + * @param field field name + * @param stream stream + * @param filename optional filename + * @return stream body part + */ + public StreamDataBodyPart streamPart(final String field, final InputStream stream, + @Nullable final String filename) { + return new StreamDataBodyPart(field, stream, filename); + } + + /** + * Create stream data part from classpath resource. {@link org.glassfish.jersey.media.multipart.FormDataBodyPart} + * could be declared as resource method parameter (but stream part can't!). + *

        + * Note: filename is extracted from provided path. + * + * @param field field name + * @param classpath classpath path + * @return stream body part + */ + public StreamDataBodyPart streamPart(final String field, final String classpath) { + final int idx1 = classpath.lastIndexOf('/'); + return streamPart(field, fromClasspath(classpath), idx1 >= 0 ? classpath.substring(idx1 + 1) : null); + } + + /** + * Multipart object builder. Useful when resource method parameter is + * {@link org.glassfish.jersey.media.multipart.FormDataMultiPart} (used to obtain all multipart fields). + * + * @return multipart object builder. + */ + public Builder multipart() { + return new Builder(this); + } + + /** + * Multipart object builder. + */ + public static class Builder { + private final MultipartArgumentHelper helper; + private final FormDataMultiPart multiPart = new FormDataMultiPart(); + + /** + * Create multipart builder. + * + * @param helper helper object + */ + public Builder(final MultipartArgumentHelper helper) { + this.helper = helper; + } + + /** + * Add a simple data part. + * + * @param field field name + * @param value field value + * @return builder instance for chained calls + */ + public Builder field(final String field, final String value) { + multiPart.bodyPart(helper.part(field, value)); + return this; + } + + /** + * Add a file part with optionally multiple files (when multiple files applied with the same field name). + * + * @param field field name + * @param files one or more files + * @return builder instance for chained calls + */ + public Builder file(final String field, final File... files) { + for (File f : files) { + multiPart.bodyPart(helper.filePart(field, f)); + } + return this; + } + + /** + * Add a file part with optionally multiple files (when multiple files applied with the same field name) + * applied from local file paths (relative to work dir). + * + * @param field filed name + * @param paths local file paths (relative to work dir) + * @return builder instance for chained calls + * @throws java.lang.IllegalStateException if file does not exist or is directory + */ + public Builder file(final String field, final String... paths) { + for (String p : paths) { + multiPart.bodyPart(helper.filePart(field, p)); + } + return this; + } + + /** + * Add a stream part with optionally multiple streams (when multiple files applied with the same field name) + * from classpath paths. + * + * @param field field name + * @param paths classpath paths + * @return builder instance for chained calls + * @throws java.lang.IllegalStateException if classpath resource not found + */ + public Builder stream(final String field, final String... paths) { + for (String p : paths) { + multiPart.bodyPart(helper.streamPart(field, p)); + } + return this; + } + + /** + * Add stream part. + *

        + * Important: file name would not be provided. + * + * @param field field name + * @param stream stream instance + * @return builder instance for chained calls + */ + public Builder stream(final String field, final InputStream stream) { + return stream(field, stream, null); + } + + /** + * Add stream part with filename. + * + * @param field field name + * @param stream stream instance + * @param filename filename + * @return builder instance for chained calls + */ + public Builder stream(final String field, final InputStream stream, @Nullable final String filename) { + multiPart.bodyPart(helper.streamPart(field, stream, filename)); + return this; + } + + /** + * @return multipart object + */ + public FormDataMultiPart build() { + Preconditions.checkState(!multiPart.getFields().isEmpty(), "Multipart must have at least one field"); + return multiPart; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartAwareCaller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartAwareCaller.java new file mode 100644 index 000000000..5d40affb9 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartAwareCaller.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.call; + +/** + * Alternative to {@link ru.vyarus.dropwizard.guice.url.util.Caller} when multipart method is called. + * Required to provide an additional helper utility to simplify multipart entities creation for method parameters. + * + * @param resource type + * @author Vyacheslav Rusakov + * @since 10.10.2025 + */ +@FunctionalInterface +public interface MultipartAwareCaller { + + /** + * Called to record resource method call. + * + * @param instance resource mock (intercepting calls) + * @param multipart multipart arguments helper utility + * @throws Exception on error + */ + void call(T instance, MultipartArgumentHelper multipart) throws Exception; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/RestCallAnalyzer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/RestCallAnalyzer.java new file mode 100644 index 000000000..79ab62cb0 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/call/RestCallAnalyzer.java @@ -0,0 +1,89 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.call; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.client.builder.FormBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder; +import ru.vyarus.dropwizard.guice.url.model.ResourceMethodInfo; +import ru.vyarus.dropwizard.guice.url.resource.ResourceAnalyzer; +import ru.vyarus.dropwizard.guice.url.util.Caller; + +/** + * Allows using direct call for a resource method to configure rest client endpoint: target url is configured from + * method {@link jakarta.ws.rs.Path} annotation. Optionally, {@link PathParam} and {@link QueryParam} parameters could + * be configured by providing non-null values. + *

        + * Limitation: this will work only with methods with DIRECTLY applied annotations: neither implemented interface method + * (when annotations only on interface) nor overridden method would work. + * + * @author Vyacheslav Rusakov + * @since 21.09.2025 + */ +public final class RestCallAnalyzer { + + private RestCallAnalyzer() { + } + + /** + * Configure rest client by direct resource method call. + *

        + * Internally, javassist proxy is used to intercept method call. + *

        + * Limitation: this will work only with methods with DIRECTLY applied annotations: neither implemented interface + * method (when annotations only on interface) nor overridden method would work. + * + * @param client rest client instance + * @param method consumer with a required method call inside + * @param body body object (could be null) + * @param resource type + * @return pre-configured builder + */ + public static TestClientRequestBuilder configure(final ResourceClient client, final Caller method, + final @Nullable Object body) { + final ResourceMethodInfo info = ResourceAnalyzer.analyzeMethodCall(client.getResourceType(), method); + Object actualBody = body; + if (actualBody == null && info.getEntity() != null) { + // use provided argument without annotations as body + // NOTE: both entity and form params are impossible + actualBody = Entity.json(info.getEntity()); + } + if (actualBody == null && !info.getFormParams().isEmpty()) { + // build form entity + final FormBuilder params = client.buildForm(info.getPath()) + .params(info.getFormParams()); + // NOTE: GET method can't declare @FormParam so all form data would be declared as @QueryParam + // force multipart form creation, even if there are no multipart values + if (!info.getConsumes().isEmpty() && info.getConsumes().contains(MediaType.MULTIPART_FORM_DATA)) { + params.forceMultipart(); + } + // assume POST + actualBody = params.buildEntity(); + } + + Preconditions.checkState(info.getHttpMethod() != null, "Called method lacks http method annotation"); + return client.build(info.getHttpMethod(), info.getPath(), actualBody) + .pathParams(info.getPathParams()) + .queryParams(info.getQueryParams()) + .matrixParams(info.getMatrixParams()) + .headers(info.getHeaderParams()) + .cookies(info.getCookieParams()); + } + + /** + * Resolve sub resource mapping path from lookup method. + * + * @param type resource type + * @param method lookup method caller + * @param resource type + * @return lookup method path + */ + public static String getSubResourcePath(final Class type, final Caller method) { + final ResourceMethodInfo info = ResourceAnalyzer.analyzeMethodCall(type, method); + return info.getPath(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestData.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestData.java new file mode 100644 index 000000000..f86e5f39c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestData.java @@ -0,0 +1,624 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import com.google.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.StackUtils; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientDefaults; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.BuilderTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.TargetTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.UriBuilderTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.util.RequestModifierSource; +import ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue; +import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Recorder request configuration data. Each applied value is recorded with an application source (line). + *

        + * All values tracked with the recorded configuration source (source line, called method), that's why all values are + * wrapped with {@link ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue}. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +@SuppressWarnings({"PMD.GodClass", "PMD.AvoidFieldNameMatchingMethodName", "PMD.TooManyMethods"}) +public class RequestData { + private static final List> INFRA = ImmutableList.of( + TestClientRequestBuilder.class, + TestClientDefaults.class, + TestClient.class, + ResourceClient.class, + RequestModifierSource.class, + TargetTracker.class, + BuilderTracker.class, + UriBuilderTracker.class, + RequestData.class + ); + private static final String DBL_TAB = "\t\t"; + + private final Runnable preRequestAction; + + @SuppressWarnings("PMD.AvoidStringBufferField") + private final StringBuilder log = new StringBuilder(); + + private final Map> queryParams = new LinkedHashMap<>(); + private final Map> matrixParams = new LinkedHashMap<>(); + private final List> pathParams = new ArrayList<>(); + private final Map> headers = new LinkedHashMap<>(); + private final Map> cookies = new LinkedHashMap<>(); + private final Map> properties = new LinkedHashMap<>(); + private final Map, SourceAwareValue> extensions = new LinkedHashMap<>(); + private final List> paths = new ArrayList<>(); + + private SourceAwareValue acceptHeader; + private SourceAwareValue languageHeader; + private SourceAwareValue encodingHeader; + private SourceAwareValue cache; + + private SourceAwareValue method; + private SourceAwareValue> entity; + private SourceAwareValue> resultMapping; + + private String url; + + /** + * Create a request data object. + * + * @param preRequestAction action to be performed before request execution (e.g. logging) + */ + public RequestData(@Nullable final Runnable preRequestAction) { + this.preRequestAction = preRequestAction; + } + + // -------------------------------------------------------------- SETTERS + + /** + * Record path configuration {@link jakarta.ws.rs.client.WebTarget#path(String)}. + * + * @param path provided path + */ + public void path(final String path) { + final SourceAwareValue value = value(path); + paths.add(value); + trace("Path", path, value.getSource()); + } + + /** + * Record path param {@link jakarta.ws.rs.client.WebTarget#resolveTemplate(String, Object)} + * (and all other variations). + * + * @param name param name + * @param value param value + * @param encodeSlashInPath true if slash encoding required + * @param encoded true if value already encoded + */ + public void resolveTemplate(final String name, final Object value, + final boolean encodeSlashInPath, final boolean encoded) { + final SourceAwareValue valueSource = value(new PathParam(name, value, encodeSlashInPath, encoded)); + pathParams.add(valueSource); + trace("Resolve template", "(encodeSlashInPath=" + encodeSlashInPath + + " encoded=" + encoded + ")\n\t\t" + name + "=" + value, valueSource.getSource()); + } + + /** + * Record multiple path params {@link jakarta.ws.rs.client.WebTarget#resolveTemplates(java.util.Map)} + * (and all other variations). + * + * @param value path parameters + * @param encodeSlashInPath true if slash encoding required + * @param encoded true if values already encoded + */ + public void resolveTemplates(final Map value, final boolean encodeSlashInPath, + final boolean encoded) { + final String source = getCallSource(); + value.forEach((s, o) -> pathParams.add( + new SourceAwareValue<>(() -> new PathParam(s, o, encodeSlashInPath, encoded), source))); + trace("Resolve template", "(encodeSlashInPath=" + encodeSlashInPath + " encoded=" + encoded + ")\n" + + toStringMap(value, DBL_TAB), source); + } + + /** + * Record matric param {@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}. + * + * @param name param name + * @param value param values + */ + public void matrixParam(final String name, final Object... value) { + final SourceAwareValue valueSource = value(value.length == 1 ? value[0] : value); + matrixParams.put(name, valueSource); + trace("Matrix param", name + "=" + (value.length == 1 ? value[0] : Arrays.toString(value)), + valueSource.getSource()); + } + + /** + * Record query param {@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}. + * + * @param name param name + * @param value param values + */ + public void queryParam(final String name, final Object... value) { + final SourceAwareValue valueSource = value(value.length == 1 ? value[0] : value); + queryParams.put(name, valueSource); + trace("Query param", name + "=" + (value.length == 1 ? value[0] : Arrays.toString(value)), + valueSource.getSource()); + } + + /** + * Expected response type {@link jakarta.ws.rs.client.WebTarget#request(String...)}, + * {@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}. + * + * @param value response types + */ + public void accept(final String... value) { + acceptHeader = value(value); + trace("Accept", Arrays.toString(value), acceptHeader.getSource()); + } + + /** + * Record property {@link jakarta.ws.rs.client.WebTarget#property(String, Object)}, + * {@link jakarta.ws.rs.client.Invocation.Builder#property(String, Object)}. + * + * @param name property name + * @param value property value + */ + public void property(final String name, final Object value) { + final SourceAwareValue valueSource = value(value); + properties.put(name, valueSource); + trace("Property", name + "=" + value, valueSource.getSource()); + } + + /** + * Record extension (by class or instance) {@link jakarta.ws.rs.client.WebTarget#register(Class)}. + * + * @param value extension class or instance + * @param priority priority + * @param contracts constracts + */ + public void register(final Object value, final int priority, final Class... contracts) { + final Class ext = value instanceof Class ? (Class) value : value.getClass(); + final SourceAwareValue valueSource = value(new Extension(ext, value, priority, contracts)); + extensions.put(ext, valueSource); + final String pr = priority > 0 ? "priority=" + priority : ""; + final String ctr = contracts.length > 0 ? " contracts=\n" + Arrays.stream(contracts) + .map(aClass -> "\t\t\t\t" + RenderUtils + .renderClassLine(aClass)).collect(Collectors.joining("\n")) : ""; + String dop = pr + ctr; + if (!dop.isEmpty()) { + dop = "\n\t\t\t" + dop; + } + trace("Register", RenderUtils.renderClassLine(ext) + dop, valueSource.getSource()); + } + + /** + * Record extension (by class or instance) {@link jakarta.ws.rs.client.WebTarget#register(Object, java.util.Map)}. + * + * @param value extension class or instance + * @param contracts contracts with priority + */ + public void register(final Object value, final Map, Integer> contracts) { + final Class ext = value instanceof Class ? (Class) value : value.getClass(); + final SourceAwareValue valueSource = value(new Extension(ext, value, contracts)); + extensions.put(ext, valueSource); + final String ctr = contracts != null ? "\n\t\t\tcontracts=\n" + toStringMap(contracts, "\t\t\t\t") : ""; + trace("Register", RenderUtils.renderClassLine(ext) + ctr, valueSource.getSource()); + } + + /** + * Record http method {@link jakarta.ws.rs.client.Invocation.Builder#method(String)} and shortcuts like + * {@link jakarta.ws.rs.client.Invocation.Builder#get()}. + * + * @param method http method + * @param entity entity + */ + public void method(final String method, final @Nullable Entity entity) { + method(method, entity, (GenericType) null); + } + + /** + * Record http method {@link jakarta.ws.rs.client.Invocation.Builder#method(String)} and shortcuts like + * {@link jakarta.ws.rs.client.Invocation.Builder#get(Class)}. + * + * @param method http method + * @param entity entity + * @param responseType requested response mapping + */ + public void method(final String method, final @Nullable Entity entity, + final @Nullable Class responseType) { + method(method, entity, responseType != null ? new GenericType<>(responseType) : null); + } + + /** + * Record http method {@link jakarta.ws.rs.client.Invocation.Builder#method(String)} and shortcuts like + * {@link jakarta.ws.rs.client.Invocation.Builder#get(jakarta.ws.rs.core.GenericType)}. + * + * @param method http method + * @param entity entity + * @param responseType requested response mapping + */ + public void method(final String method, final @Nullable Entity entity, + final @Nullable GenericType responseType) { + this.method = value(method); + this.entity = entity != null ? value(entity) : null; + this.resultMapping = responseType != null ? value(responseType) : null; + trace("Method", method + (responseType != null ? " type=" + TypeToStringUtils + .toStringType(responseType.getType(), null) : "") + + (entity != null ? " entity=" + entity : ""), this.method.getSource()); + // this is the last point where configuration could be recorded + // NOTE: invocation is not tracked, so in case of .buildGet().invoke(Some.class) class mapping + // would not be tracked (should not be a problem) + beforeRequest(); + } + + /** + * Register required language {@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}. + * + * @param value required languages + */ + public void language(final String... value) { + languageHeader = value(value); + trace("Accept Language", Arrays.toString(value), languageHeader.getSource()); + } + + /** + * Register required encoding {@link jakarta.ws.rs.client.Invocation.Builder#acceptEncoding(String...)}. + * + * @param value required encodings + */ + public void encoding(final String... value) { + encodingHeader = value(value); + trace("Accept Encoding", Arrays.toString(value), encodingHeader.getSource()); + } + + /** + * Register cookie {@link jakarta.ws.rs.client.Invocation.Builder#cookie(String, String)}. + * + * @param value cookie + */ + public void cookie(final Cookie value) { + final SourceAwareValue valueSource = value(value); + cookies.put(value.getName(), valueSource); + trace("Cookie", RuntimeDelegate.getInstance().createHeaderDelegate(Cookie.class).toString(value), + valueSource.getSource()); + } + + /** + * Register cache control + * {@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(jakarta.ws.rs.core.CacheControl)}. + * + * @param value cache control + */ + public void cacheControl(final CacheControl value) { + cache = value(value); + trace("Cache", RuntimeDelegate.getInstance() + .createHeaderDelegate(CacheControl.class).toString(value), cache.getSource()); + } + + /** + * Register header {@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}. + * + * @param name header name + * @param value header value + */ + public void header(final String name, final Object value) { + final SourceAwareValue valueSource = value(value); + headers.put(name, valueSource); + trace("Header", name + "=" + value, valueSource.getSource()); + } + + /** + * Record multiple headers + * {@link jakarta.ws.rs.client.Invocation.Builder#headers(jakarta.ws.rs.core.MultivaluedMap)}. + * + * @param value headers + */ + public void headers(final MultivaluedMap value) { + final String source = getCallSource(); + value.forEach((s, o) -> headers + .put(s, new SourceAwareValue<>(() -> o.size() == 1 ? o.get(0) : o, source))); + trace("Headers", toStringMap(value, DBL_TAB), source); + } + + /** + * Record target url. Called after {@link jakarta.ws.rs.client.WebTarget#request()}. + * + * @param url url + */ + public void url(final String url) { + this.url = url; + } + + // -------------------------------------------------------------- GETTERS + + + /** + * @return query params + */ + public Map> getQueryParams() { + return queryParams; + } + + /** + * @return matric params + */ + public Map> getMatrixParams() { + return matrixParams; + } + + /** + * @return path params + */ + public List> getPathParams() { + return pathParams; + } + + /** + * @return headers + */ + public Map> getHeaders() { + return headers; + } + + /** + * @return cookies + */ + public Map> getCookies() { + return cookies; + } + + /** + * @return properties + */ + public Map> getProperties() { + return properties; + } + + /** + * @return extensions + */ + public Map, SourceAwareValue> getExtensions() { + return extensions; + } + + /** + * @return paths + */ + public List> getPaths() { + return paths; + } + + /** + * @return accept header values + */ + @Nullable + public SourceAwareValue getAcceptHeader() { + return acceptHeader; + } + + /** + * @return accept language header values + */ + @Nullable + public SourceAwareValue getLanguageHeader() { + return languageHeader; + } + + /** + * @return accept encoding header values + */ + @Nullable + public SourceAwareValue getEncodingHeader() { + return encodingHeader; + } + + /** + * @return cache control header value + */ + @Nullable + public SourceAwareValue getCache() { + return cache; + } + + /** + * @return called http method + */ + @Nullable + public SourceAwareValue getMethod() { + return method; + } + + /** + * @return used entity + */ + @Nullable + public SourceAwareValue> getEntity() { + return entity; + } + + /** + * Covers both {@link java.lang.Class} and {@link jakarta.ws.rs.core.GenericType} options (in case of class, + * use generic type constructor). + * + * @return result mapping (if specified) + */ + @Nullable + public SourceAwareValue> getResultMapping() { + return resultMapping; + } + + /** + * @return target url + */ + @Nullable + public String getUrl() { + return url; + } + + /** + * @return modifications log + */ + public String getLog() { + return log.toString(); + } + + private String getCallSource() { + return StackUtils.getCallerSource(INFRA); + } + + @SuppressFBWarnings("VA_FORMAT_STRING_USES_NEWLINE") + private void trace(final String title, final String value, final String source) { + log.append(String.format("\t%-40s %s\n\t\t%s\n\n", title, source == null ? "" : source, + value.startsWith(DBL_TAB) ? value.substring(2) : value)); + } + + private String toStringMap(final Map map, final String prefix) { + return map.entrySet().stream().map(entry -> + String.format("%s%s=%s", prefix, + entry.getKey() instanceof Class + ? RenderUtils.renderClassLine((Class) entry.getKey()) + : entry.getKey(), + entry.getValue())) + .collect(Collectors.joining("\n")); + } + + private SourceAwareValue value(final T supplier) { + return new SourceAwareValue<>(() -> supplier, getCallSource()); + } + + private void beforeRequest() { + if (preRequestAction != null) { + preRequestAction.run(); + } + } + + /** + * Path parameter. + */ + public static class PathParam { + private final String name; + private final Object value; + private final boolean encodeSlashInPath; + private final boolean encoded; + + /** + * Create a parameter info object. + * + * @param name param name + * @param value param value + * @param encodeSlashInPath true to encode slashes + * @param encoded true if value is already encoded + */ + public PathParam(final String name, final Object value, + final boolean encodeSlashInPath, final boolean encoded) { + this.name = name; + this.value = value; + this.encodeSlashInPath = encodeSlashInPath; + this.encoded = encoded; + } + + /** + * @return param name + */ + public String getName() { + return name; + } + + /** + * @return param value + */ + public Object getValue() { + return value; + } + + /** + * @return true if slashes encoding is required + */ + public boolean isEncodeSlashInPath() { + return encodeSlashInPath; + } + + /** + * @return true if value already encoded + */ + public boolean isEncoded() { + return encoded; + } + } + + /** + * Extension info. + */ + public static class Extension { + private final Class type; + private final Object value; + private final Map, Integer> contracts; + + /** + * Create extension info. + * + * @param type extension class + * @param value extension instance or clas + * @param priority priority + * @param contracts extension contracts + */ + public Extension(final Class type, final Object value, final int priority, final Class... contracts) { + this.type = type; + this.value = value; + this.contracts = new HashMap<>(); + for (Class contract : contracts) { + this.contracts.put(contract, priority); + } + } + + /** + * Create extension info. + * + * @param type extension class + * @param value extension instance or clas + * @param contracts extension contracts with priority + */ + public Extension(final Class type, final Object value, final Map, Integer> contracts) { + this.type = type; + this.value = value; + this.contracts = contracts; + } + + /** + * @return extension class + */ + public Class getType() { + return type; + } + + /** + * @return extension instance or class + */ + public Object getValue() { + return value; + } + + /** + * @return extension contracts with priority + */ + public Map, Integer> getContracts() { + return contracts; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestTracker.java new file mode 100644 index 000000000..e03159cd1 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/RequestTracker.java @@ -0,0 +1,376 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.TargetTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.TrackableData; +import ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue; +import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Tracks jersey client api configuration. Could be used to record all applied changes for + * {@link jakarta.ws.rs.client.WebTarget} and {@link jakarta.ws.rs.client.Invocation.Builder}. + *

        + * Limitations: rx and async apis are not tracked. Also, invocation calls not handled, so for example, in case of + * {@code buildGet().invoke(Some.class)}, requested mapping class would not be recorded because + * {@link jakarta.ws.rs.client.Invocation} object is not tracked. + *

        + * For example, to track real request configuration: + *

        
        + *     RequestTracker tracker = new RequestTracker();
        + *     WebTarget target = tracker.track(originalTarget);
        + *     target.path("..").request().get();
        + *     // print changes to console
        + *     System.out.println(tracker.getLog());
        + * 
        + *

        + * {@link #track()} could be used to record modifications for a target mock. This could be used to test + * request configuration api (when only applied changes must be verified). + *

        + * {@link Runnable} could be used to execute something just before request execution (when all data collected): + *

        
        + *      RequestTracker tracker = new RequestTracker()'
        + *      WebTarget target = tracker.track(originalTarget, ()-> System.out.println(tracker.getLog()));
        + * 
        + *

        + * Tracker could be resolved from tarcked {@link jakarta.ws.rs.client.WebTarget} and + * {@link jakarta.ws.rs.client.Invocation.Builder} objects with + * {@link #lookupTracker(jakarta.ws.rs.client.WebTarget)} and + * {@link #lookupTracker(jakarta.ws.rs.client.Invocation.Builder)}. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +public class RequestTracker { + + private RequestData data; + + /** + * Lookup tracker in request builder object. + * + * @param object request object + * @return tracker or null if the object doesn't support tracking + */ + public static Optional lookupTracker(final WebTarget object) { + return getTracker(object); + } + + /** + * Lookup tracker in request builder object. + * + * @param object request object + * @return tracker or null if the object doesn't support tracking + */ + public static Optional lookupTracker(final Invocation.Builder object) { + return getTracker(object); + } + + /** + * Record request changes without actual request execution. Useful for testing apis, modifying request. + * Use mocker {@link jakarta.ws.rs.client.WebTarget} inside + * ({@link ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock}) which does not implement + * request processing methods (just accept configuration). + * + * @return tracked target + */ + public WebTarget track() { + return track((Runnable) null); + } + + /** + * Record request changes without actual request execution. Useful for testing apis, modifying request. + * Use mocker {@link jakarta.ws.rs.client.WebTarget} inside + * ({@link ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock}) which does not implement + * request processing methods (just accept configuration). + * + * @param preRequestAction action to execute before request execution (could be used to log changes) + * @return tracked target + */ + public WebTarget track(final @Nullable Runnable preRequestAction) { + data = new RequestData(preRequestAction); + return new TargetTracker(this); + } + + /** + * Track real request. Useful to track request configuration (with sources). + *

        + * IMPORTANT: tracked target object can't be re-used (it does not create new target instances for each call + * (whereas original target is correctly re-created). + * + * @param target original web target + * @return tracked target + */ + public WebTarget track(final WebTarget target) { + return track(target, null); + } + + /** + * Track real request. Useful to track request configuration (with sources). + *

        + * IMPORTANT: tracked target object can't be re-used (it does not create new target instances for each call + * (whereas original target is correctly re-created). + * + * @param target original web target + * @param preRequestAction action to execute before request execution (could be used to log changes) + * @return tracked target + */ + public WebTarget track(final WebTarget target, final @Nullable Runnable preRequestAction) { + data = new RequestData(preRequestAction); + return new TargetTracker(this, target); + } + + // ------------------------------------------------------------------------ TRACKED DATA + + /** + * {@link jakarta.ws.rs.client.WebTarget#queryParam(String, Object...)}. + *

        + * Note: returned value may contain array if multiple values configured. + * + * @return applied query params + */ + public Map getQueryParams() { + // map may contain nulls + final Map res = new HashMap<>(); + data.getQueryParams().forEach((s, val) -> res.put(s, val.get())); + return res; + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#matrixParam(String, Object...)}. + *

        + * Note: returned value may contain array if multiple values configured. + * + * @return applied matric params + */ + public Map getMatrixParams() { + // map may contain nulls + final Map res = new HashMap<>(); + data.getMatrixParams().forEach((s, val) -> res.put(s, val.get())); + return res; + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#resolveTemplate(String, Object)} (and all other variations). + *

        + * See {@link RequestData#getPathParams()} for encoding info. + * + * @return applied path params + */ + public Map getPathParams() { + return data.getPathParams().stream() + .collect(Collectors.toMap(o -> o.get().getName(), + o -> o.get().getValue())); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#header(String, Object)}. + * + * @return applied headers + */ + public Map getHeaders() { + return data.getHeaders().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#cookie(String, String)}. + * + * @return applied cookies + */ + public Map getCookies() { + return data.getCookies().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#property(String, Object)}, + * {@link jakarta.ws.rs.client.Invocation.Builder#property(String, Object)}. + * + * @return applied properties + */ + public Map getProperties() { + return data.getProperties().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#register(Class)}. + *

        + * Values in returned map could be a class or instance (if instance was used for configuration). + *

        + * See {@link RequestData#getExtensions()} for applied contracts. + * + * @return applied extensions + */ + public Map, Object> getExtensions() { + return data.getExtensions().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> e.getValue().get().getValue())); + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#path(String)}. + *

        + * NOTE: does not count {@link jakarta.ws.rs.client.WebTarget#getUriBuilder()} changes. + * + * @return applied paths + */ + public List getPaths() { + return data.getPaths().stream().map(SourceAwareValue::get).collect(Collectors.toList()); + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#request(String...)}, + * {@link jakarta.ws.rs.client.Invocation.Builder#accept(String...)}. + * + * @return applied expected response types + */ + public List getAcceptHeader() { + return data.getAcceptHeader() == null ? Collections.emptyList() + : Arrays.asList(data.getAcceptHeader().get()); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#acceptLanguage(String...)}. + * + * @return applied expected languages + */ + public List getLanguageHeader() { + return data.getLanguageHeader() == null ? Collections.emptyList() + : Arrays.asList(data.getLanguageHeader().get()); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#acceptEncoding(String...)}. + * + * @return applied expected encodings + */ + public List getEncodingHeader() { + return data.getEncodingHeader() == null ? Collections.emptyList() + : Arrays.asList(data.getEncodingHeader().get()); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(jakarta.ws.rs.core.CacheControl)}. + * + * @return applied cache control object (cache header) + * @see #getCacheHeader() + */ + public CacheControl getCache() { + return data.getCache() == null ? null : data.getCache().get(); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#cacheControl(jakarta.ws.rs.core.CacheControl)}. + * + * @return applied cache header + * @see #getCache() + */ + public String getCacheHeader() { + final CacheControl cc = getCache(); + return cc == null ? null + : RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class).toString(cc); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#method(String)} and shortcuts like + * {@link jakarta.ws.rs.client.Invocation.Builder#get()}. + * + * @return request HTTP method or null (if request method wasn't configred) + */ + @Nullable + public String getHttpMethod() { + return data.getMethod() == null ? null : data.getMethod().get(); + } + + /** + * {@link jakarta.ws.rs.client.Invocation.Builder#method(String, jakarta.ws.rs.client.Entity)} and shortcuts like + * {@link jakarta.ws.rs.client.Invocation.Builder#post(jakarta.ws.rs.client.Entity)}. + * + * @return request entity or null + */ + @Nullable + public Entity getEntity() { + return data.getEntity() == null ? null : data.getEntity().get(); + } + + /** + * Result mapping from apis like {@link jakarta.ws.rs.client.Invocation.Builder#get(Class)}. + * + * @return result mapping type or null + */ + @Nullable + public Type getResultMapping() { + return data.getResultMapping() == null ? null : data.getResultMapping().get().getType(); + } + + /** + * Result mapping from apis like {@link jakarta.ws.rs.client.Invocation.Builder#get(Class)}. + * + * @return result class (for {@link jakarta.ws.rs.core.GenericType} it would be just a root class without generics) + */ + @Nullable + public Class getResultMappingClass() { + return data.getResultMapping() == null ? null : data.getResultMapping().get().getRawType(); + } + + /** + * Result mapping from apis like {@link jakarta.ws.rs.client.Invocation.Builder#get(Class)}. + * + * @return string representation of resulted type, including generics (like "List<Something>") + */ + @Nullable + public String getResultMappingString() { + return data.getResultMapping() == null ? null : TypeToStringUtils + .toStringType(data.getResultMapping().get().getType(), null); + } + + /** + * {@link jakarta.ws.rs.client.WebTarget#getUri()}. + *

        + * Url is recorded only after invocation builder creation with {@link jakarta.ws.rs.client.WebTarget#request()}. + * + * @return the resulted url or null + */ + @Nullable + public String getUrl() { + return data.getUrl(); + } + + /** + * Log shows what changed, where and at what order. + * + * @return request modifications log (with source references) + */ + public String getLog() { + final String log = data.getLog(); + return "\n" + (log.isEmpty() ? "\tNo configurations" : log); + } + + /** + * Could be used when modification source data is also required (shortcuts above remove modification source info). + * + * @return raw collected data object + */ + public RequestData getRawData() { + return data; + } + + private static Optional getTracker(final Object object) { + return Optional.ofNullable(object instanceof TrackableData ? ((TrackableData) object).getTracker() : null); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/BuilderTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/BuilderTracker.java new file mode 100644 index 000000000..f7f1608aa --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/BuilderTracker.java @@ -0,0 +1,340 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl; + +import jakarta.ws.rs.client.AsyncInvoker; +import jakarta.ws.rs.client.CompletionStageRxInvoker; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.RxInvoker; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestData; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; + +import java.util.Arrays; +import java.util.Locale; + +/** + * {@link jakarta.ws.rs.client.Invocation.Builder} object tracker. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.ExcessivePublicCount", "PMD.TooManyMethods", + "PMD.AvoidFieldNameMatchingMethodName"}) +public class BuilderTracker implements Invocation.Builder, TrackableData { + + private static final String GET = "GET"; + private static final String DELETE = "DELETE"; + private static final String POST = "POST"; + private static final String PUT = "PUT"; + private static final String HEAD = "HEAD"; + private static final String OPTIONS = "OPTIONS"; + private static final String TRACE = "TRACE"; + + private final RequestTracker tracker; + private final RequestData data; + private final Invocation.Builder target; + + /** + * Create a request builder tracker. + * + * @param tracker tracker object + * @param builder real builder instance + */ + public BuilderTracker(final RequestTracker tracker, final Invocation.Builder builder) { + this.tracker = tracker; + this.data = tracker.getRawData(); + this.target = builder; + } + + @Override + public Invocation build(final String method) { + data.method(method, null); + return target.build(method); + } + + @Override + public Invocation build(final String method, final Entity entity) { + data.method(method, entity); + return target.build(method, entity); + } + + @Override + public Invocation buildGet() { + data.method(GET, null); + return target.buildGet(); + } + + @Override + public Invocation buildDelete() { + data.method(DELETE, null); + return target.buildDelete(); + } + + @Override + public Invocation buildPost(final Entity entity) { + data.method(POST, entity); + return target.buildPost(entity); + } + + @Override + public Invocation buildPut(final Entity entity) { + data.method(PUT, entity); + return target.buildPut(entity); + } + + @Override + public AsyncInvoker async() { + // intentionally not tracked + return target.async(); + } + + @Override + public Invocation.Builder accept(final String... mediaTypes) { + data.accept(mediaTypes); + target.accept(mediaTypes); + return this; + } + + @Override + public Invocation.Builder accept(final MediaType... mediaTypes) { + data.accept(Arrays.stream(mediaTypes).map(MediaType::toString).toArray(String[]::new)); + target.accept(mediaTypes); + return this; + } + + @Override + public Invocation.Builder acceptLanguage(final Locale... locales) { + data.language(Arrays.stream(locales).map(Locale::toString).toArray(String[]::new)); + target.acceptLanguage(locales); + return this; + } + + @Override + public Invocation.Builder acceptLanguage(final String... locales) { + data.language(locales); + target.acceptLanguage(locales); + return this; + } + + @Override + public Invocation.Builder acceptEncoding(final String... encodings) { + data.encoding(encodings); + target.acceptEncoding(encodings); + return this; + } + + @Override + public Invocation.Builder cookie(final Cookie cookie) { + data.cookie(cookie); + target.cookie(cookie); + return this; + } + + @Override + public Invocation.Builder cookie(final String name, final String value) { + cookie(new NewCookie.Builder(name).value(value).build()); + target.cookie(name, value); + return this; + } + + @Override + public Invocation.Builder cacheControl(final CacheControl cacheControl) { + data.cacheControl(cacheControl); + target.cacheControl(cacheControl); + return this; + } + + @Override + public Invocation.Builder header(final String name, final Object value) { + data.header(name, value); + target.header(name, value); + return this; + } + + @Override + public Invocation.Builder headers(final MultivaluedMap headers) { + data.headers(headers); + target.headers(headers); + return this; + } + + @Override + public Invocation.Builder property(final String name, final Object value) { + data.property(name, value); + target.property(name, value); + return this; + } + + @Override + public CompletionStageRxInvoker rx() { + // intentionally not tracked + return target.rx(); + } + + @Override + public T rx(final Class clazz) { + // intentionally not tracked + return target.rx(clazz); + } + + @Override + public Response get() { + data.method(GET, null); + return target.get(); + } + + @Override + public T get(final Class responseType) { + data.method(GET, null, responseType); + return target.get(responseType); + } + + @Override + public T get(final GenericType responseType) { + data.method(GET, null, responseType); + return target.get(responseType); + } + + @Override + public Response put(final Entity entity) { + data.method(PUT, entity); + return target.put(entity); + } + + @Override + public T put(final Entity entity, final Class responseType) { + data.method(PUT, entity, responseType); + return target.put(entity, responseType); + } + + @Override + public T put(final Entity entity, final GenericType responseType) { + data.method(PUT, entity, responseType); + return target.put(entity, responseType); + } + + @Override + public Response post(final Entity entity) { + data.method(POST, entity); + return target.post(entity); + } + + @Override + public T post(final Entity entity, final Class responseType) { + data.method(POST, entity, responseType); + return target.post(entity, responseType); + } + + @Override + public T post(final Entity entity, final GenericType responseType) { + data.method(POST, entity, responseType); + return target.post(entity, responseType); + } + + @Override + public Response delete() { + data.method(DELETE, null); + return target.delete(); + } + + @Override + public T delete(final Class responseType) { + data.method(DELETE, null, responseType); + return target.delete(responseType); + } + + @Override + public T delete(final GenericType responseType) { + data.method(DELETE, null, responseType); + return target.delete(responseType); + } + + @Override + public Response head() { + data.method(HEAD, null); + return target.head(); + } + + @Override + public Response options() { + data.method(OPTIONS, null); + return target.options(); + } + + @Override + public T options(final Class responseType) { + data.method(OPTIONS, null, responseType); + return target.options(responseType); + } + + @Override + public T options(final GenericType responseType) { + data.method(OPTIONS, null, responseType); + return target.options(responseType); + } + + @Override + public Response trace() { + data.method(TRACE, null); + return target.trace(); + } + + @Override + public T trace(final Class responseType) { + data.method(TRACE, null, responseType); + return target.trace(responseType); + } + + @Override + public T trace(final GenericType responseType) { + data.method(TRACE, null, responseType); + return target.trace(responseType); + } + + @Override + public Response method(final String name) { + data.method(name, null); + return target.method(name); + } + + @Override + public T method(final String name, final Class responseType) { + data.method(name, null, responseType); + return target.method(name, responseType); + } + + @Override + public T method(final String name, final GenericType responseType) { + data.method(name, null, responseType); + return target.method(name, responseType); + } + + @Override + public Response method(final String name, final Entity entity) { + data.method(name, entity); + return target.method(name, entity); + } + + @Override + public T method(final String name, final Entity entity, final Class responseType) { + data.method(name, entity, responseType); + return target.method(name, entity, responseType); + } + + @Override + public T method(final String name, final Entity entity, final GenericType responseType) { + data.method(name, entity, responseType); + return target.method(name, entity, responseType); + } + + @Override + public RequestTracker getTracker() { + return tracker; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TargetTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TargetTracker.java new file mode 100644 index 000000000..2097ded98 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TargetTracker.java @@ -0,0 +1,227 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestData; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock; + +import java.net.URI; +import java.util.Arrays; +import java.util.Map; + +/** + * {@link jakarta.ws.rs.client.WebTarget} object tracker. Used to intercept configuration calls and verify + * correctness in tests. Does not support direct async and rx apis. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +@SuppressWarnings({"PMD.TooManyMethods", "PMD.AvoidFieldNameMatchingMethodName"}) +public class TargetTracker implements WebTarget, TrackableData { + + private final RequestTracker tracker; + private final RequestData data; + private WebTarget target; + private UriBuilder uriBuilder; + + /** + * Create an empty web target wrapper (wihtout real target). + * + * @param tracker request tracker object + */ + public TargetTracker(final RequestTracker tracker) { + this(tracker, null); + } + + /** + * Create a web target wrapper. + * + * @param tracker request tracker object + * @param target real target object + */ + public TargetTracker(final RequestTracker tracker, @Nullable final WebTarget target) { + this.tracker = tracker; + this.data = tracker.getRawData(); + // in case of null just a mock to avoid null + this.target = target == null ? new TargetMock() : target; + uriBuilder = new UriBuilderTracker(data, this.target.getUriBuilder()); + } + + @Override + public URI getUri() { + return target.getUri(); + } + + @Override + public UriBuilder getUriBuilder() { + return uriBuilder; + } + + @Override + public WebTarget path(final String path) { + data.path(path); + target(target.path(path)); + return this; + } + + @Override + public WebTarget resolveTemplate(final String name, final Object value) { + data.resolveTemplate(name, value, false, false); + target(target.resolveTemplate(name, value)); + return this; + } + + @Override + public WebTarget resolveTemplate(final String name, final Object value, final boolean encodeSlashInPath) { + data.resolveTemplate(name, value, encodeSlashInPath, false); + target(target.resolveTemplate(name, value, encodeSlashInPath)); + return this; + } + + @Override + public WebTarget resolveTemplateFromEncoded(final String name, final Object value) { + data.resolveTemplate(name, value, false, true); + target(target.resolveTemplateFromEncoded(name, value)); + return this; + } + + @Override + public WebTarget resolveTemplates(final Map templateValues) { + data.resolveTemplates(templateValues, false, false); + target(target.resolveTemplates(templateValues)); + return this; + } + + @Override + public WebTarget resolveTemplates(final Map templateValues, final boolean encodeSlashInPath) { + data.resolveTemplates(templateValues, encodeSlashInPath, false); + target(target.resolveTemplates(templateValues, encodeSlashInPath)); + return this; + } + + @Override + public WebTarget resolveTemplatesFromEncoded(final Map templateValues) { + data.resolveTemplates(templateValues, false, true); + target(target.resolveTemplatesFromEncoded(templateValues)); + return this; + } + + @Override + public WebTarget matrixParam(final String name, final Object... values) { + data.matrixParam(name, values); + target(target.matrixParam(name, values)); + return this; + } + + @Override + public WebTarget queryParam(final String name, final Object... values) { + data.queryParam(name, values); + target(target.queryParam(name, values)); + return this; + } + + @Override + public Invocation.Builder request() { + data.url(target.getUri().toString()); + return new BuilderTracker(tracker, target.request()); + } + + @Override + public Invocation.Builder request(final String... acceptedResponseTypes) { + data.accept(acceptedResponseTypes); + data.url(target.getUri().toString()); + return new BuilderTracker(tracker, target.request(acceptedResponseTypes)); + } + + @Override + public Invocation.Builder request(final MediaType... acceptedResponseTypes) { + data.accept(Arrays.stream(acceptedResponseTypes).map(MediaType::toString).toArray(String[]::new)); + data.url(target.getUri().toString()); + return new BuilderTracker(tracker, target.request(acceptedResponseTypes)); + } + + @Override + public Configuration getConfiguration() { + return target.getConfiguration(); + } + + @Override + public WebTarget property(final String name, final Object value) { + data.property(name, value); + target(target.property(name, value)); + return this; + } + + @Override + public WebTarget register(final Class componentClass) { + data.register(componentClass, -1); + target(target.register(componentClass)); + return this; + } + + @Override + public WebTarget register(final Class componentClass, final int priority) { + data.register(componentClass, priority); + target(target.register(componentClass, priority)); + return this; + } + + @Override + public WebTarget register(final Class componentClass, final Class... contracts) { + data.register(componentClass, -1, contracts); + target(target.register(componentClass, contracts)); + return this; + } + + @Override + public WebTarget register(final Class componentClass, final Map, Integer> contracts) { + data.register(componentClass, contracts); + target(target.register(componentClass, contracts)); + return this; + } + + @Override + public WebTarget register(final Object component) { + data.register(component, -1); + target(target.register(component)); + return this; + } + + @Override + public WebTarget register(final Object component, final int priority) { + data.register(component, priority); + target(target.register(component, priority)); + return this; + } + + @Override + public WebTarget register(final Object component, final Class... contracts) { + data.register(component, -1, contracts); + target(target.register(component, contracts)); + return this; + } + + @Override + public WebTarget register(final Object component, final Map, Integer> contracts) { + data.register(component, contracts); + target(target.register(component, contracts)); + return this; + } + + @Override + public RequestTracker getTracker() { + return tracker; + } + + private void target(final WebTarget target) { + if (target == null) { + return; + } + this.target = target; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TrackableData.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TrackableData.java new file mode 100644 index 000000000..bdf52e65c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/TrackableData.java @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl; + +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; + +/** + * Identifies request objects, containing tracker. See + * {@link RequestTracker#lookupTracker(jakarta.ws.rs.client.WebTarget)} and + * {@link RequestTracker#lookupTracker(jakarta.ws.rs.client.Invocation.Builder)}. + * + * @author Vyacheslav Rusakov + * @since 07.10.2025 + */ +public interface TrackableData { + + /** + * @return tracker object + */ + RequestTracker getTracker(); +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/UriBuilderTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/UriBuilderTracker.java new file mode 100644 index 000000000..5852d7f4e --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/UriBuilderTracker.java @@ -0,0 +1,293 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.UriBuilderException; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestData; +import ru.vyarus.dropwizard.guice.url.resource.ResourceAnalyzer; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Map; + +/** + * {@link jakarta.ws.rs.core.UriBuilder} tracker for + * {@link ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker}. + *

        + * Path modification like {@link #scheme(String)} or {@link #replacePath(String)} or + * {@link #replaceQueryParam(String, Object...)} are tracked as simple path or parameter assignment. + * Total accuracy is not important: it's important to DETECT all changes and show the calling source for them. + * + * @author Vyacheslav Rusakov + * @since 06.10.2025 + */ +@SuppressWarnings("PMD.TooManyMethods") +public class UriBuilderTracker extends UriBuilder { + + private final RequestData data; + private final UriBuilder builder; + + /** + * Create builder tracker. + * + * @param data data collector + * @param builder real builder + */ + public UriBuilderTracker(final RequestData data, final UriBuilder builder) { + this.data = data; + this.builder = builder; + } + + @SuppressWarnings({"checkstyle:NoClone", "PMD.CloneMethodMustImplementCloneable"}) + @Override + public UriBuilderTracker clone() { + throw new UnsupportedOperationException(); + } + + @Override + public UriBuilder uri(final URI uri) { + data.path(uri.toString()); + builder.uri(uri); + return this; + } + + @Override + public UriBuilder uri(final String uriTemplate) { + data.path(uriTemplate); + builder.uri(uriTemplate); + return this; + } + + @Override + public UriBuilder scheme(final String scheme) { + // not correct by does not matter + data.path(scheme); + builder.scheme(scheme); + return this; + } + + @Override + public UriBuilder schemeSpecificPart(final String ssp) { + data.path(ssp); + builder.schemeSpecificPart(ssp); + return this; + } + + @Override + public UriBuilder userInfo(final String ui) { + data.path(ui); + builder.userInfo(ui); + return this; + } + + @Override + public UriBuilder host(final String host) { + data.path(host); + builder.host(host); + return this; + } + + @Override + public UriBuilder port(final int port) { + data.path(String.valueOf(port)); + builder.port(port); + return this; + } + + @Override + public UriBuilder replacePath(final String path) { + data.path(path); + builder.replacePath(path); + return this; + } + + @Override + public UriBuilder path(final String path) { + data.path(path); + builder.path(path); + return this; + } + + @Override + public UriBuilder path(final Class resource) { + data.path(ResourceAnalyzer.getResourcePath(resource)); + builder.path(resource.getName()); + return this; + } + + @Override + public UriBuilder path(final Class resource, final String method) { + data.path(ResourceAnalyzer.getMethodPath(resource, method)); + builder.path(resource.getName() + "#" + method); + return this; + } + + @Override + public UriBuilder path(final Method method) { + data.path(ResourceAnalyzer.getMethodPath(method)); + builder.path(method); + return this; + } + + @Override + public UriBuilder segment(final String... segments) { + data.path(PathUtils.path(segments)); + builder.segment(segments); + return this; + } + + @Override + public UriBuilder replaceMatrix(final String matrix) { + if (matrix != null) { + final Multimap vals = parseParams(matrix, ";"); + vals.asMap().forEach((s, strings) -> data + .matrixParam(s, strings.size() == 1 ? new Object[]{strings.iterator().next()} : strings.toArray())); + } + builder.replaceMatrix(matrix); + return this; + } + + @Override + public UriBuilder matrixParam(final String name, final Object... values) { + data.matrixParam(name, values); + builder.matrixParam(name, values); + return this; + } + + @Override + public UriBuilder replaceMatrixParam(final String name, final Object... values) { + data.matrixParam(name, values); + builder.replaceMatrixParam(name, values); + return this; + } + + @Override + public UriBuilder replaceQuery(final String query) { + if (query != null) { + final Multimap vals = parseParams(query, "&"); + vals.asMap().forEach((name, values) -> data + .queryParam(name, values.size() == 1 ? new Object[]{values.iterator().next()} : values.toArray())); + } + builder.replaceQuery(query); + return this; + } + + @Override + public UriBuilder queryParam(final String name, final Object... values) { + data.queryParam(name, values); + builder.queryParam(name, values); + return this; + } + + @Override + public UriBuilder replaceQueryParam(final String name, final Object... values) { + data.queryParam(name, values); + builder.replaceQueryParam(name, values); + return this; + } + + @Override + public UriBuilder fragment(final String fragment) { + data.path(fragment); + builder.fragment(fragment); + return this; + } + + @Override + public UriBuilder resolveTemplate(final String name, final Object value) { + data.resolveTemplate(name, value, false, false); + builder.resolveTemplate(name, value); + return this; + } + + @Override + public UriBuilder resolveTemplate(final String name, final Object value, final boolean encodeSlashInPath) { + data.resolveTemplate(name, value, encodeSlashInPath, false); + builder.resolveTemplate(name, value, encodeSlashInPath); + return this; + } + + @Override + public UriBuilder resolveTemplateFromEncoded(final String name, final Object value) { + data.resolveTemplate(name, value, false, true); + builder.resolveTemplateFromEncoded(name, value); + return this; + } + + @Override + public UriBuilder resolveTemplates(final Map templateValues) { + data.resolveTemplates(templateValues, false, false); + builder.resolveTemplates(templateValues); + return this; + } + + @Override + public UriBuilder resolveTemplates(final Map templateValues, + final boolean encodeSlashInPath) throws IllegalArgumentException { + data.resolveTemplates(templateValues, encodeSlashInPath, false); + builder.resolveTemplates(templateValues, encodeSlashInPath); + return this; + } + + @Override + public UriBuilder resolveTemplatesFromEncoded(final Map templateValues) { + data.resolveTemplates(templateValues, false, true); + builder.resolveTemplatesFromEncoded(templateValues); + return this; + } + + // ---------------------------------------------------------------------- NOT TRACKED + + @Override + public URI buildFromMap(final Map values) { + return builder.buildFromMap(values); + } + + @Override + public URI buildFromMap(final Map values, final boolean encodeSlashInPath) + throws IllegalArgumentException, UriBuilderException { + return builder.buildFromMap(values, encodeSlashInPath); + } + + @Override + public URI buildFromEncodedMap(final Map values) throws IllegalArgumentException, UriBuilderException { + return builder.buildFromEncoded(values); + } + + @Override + public URI build(final Object... values) throws IllegalArgumentException, UriBuilderException { + return builder.build(values); + } + + @Override + public URI build(final Object[] values, final boolean encodeSlashInPath) + throws IllegalArgumentException, UriBuilderException { + return builder.build(values, encodeSlashInPath); + } + + @Override + public URI buildFromEncoded(final Object... values) throws IllegalArgumentException, UriBuilderException { + return builder.buildFromEncoded(values); + } + + @Override + public String toTemplate() { + return builder.toTemplate(); + } + + private Multimap parseParams(final String params, final String separator) { + final String[] pars = params.split(separator); + final Multimap vals = HashMultimap.create(); + for (String param : pars) { + final String[] parts = param.split("="); + if (parts.length == 2) { + vals.put(parts[0], parts[1]); + } else { + vals.put(parts[0], null); + } + } + return vals; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/BuilderMock.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/BuilderMock.java new file mode 100644 index 000000000..b2aeeb6ad --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/BuilderMock.java @@ -0,0 +1,251 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock; + +import jakarta.ws.rs.client.AsyncInvoker; +import jakarta.ws.rs.client.CompletionStageRxInvoker; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.RxInvoker; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +import java.util.Locale; + +/** + * {@link jakarta.ws.rs.client.Invocation.Builder} mock object. Used to track configuration correctness in + * {@link ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker} without real target. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.ExcessivePublicCount", "PMD.TooManyMethods"}) +public class BuilderMock implements Invocation.Builder { + + @Override + public Invocation build(final String method) { + return null; + } + + @Override + public Invocation build(final String method, final Entity entity) { + return null; + } + + @Override + public Invocation buildGet() { + return null; + } + + @Override + public Invocation buildDelete() { + return null; + } + + @Override + public Invocation buildPost(final Entity entity) { + return null; + } + + @Override + public Invocation buildPut(final Entity entity) { + return null; + } + + @Override + public AsyncInvoker async() { + return null; + } + + @Override + public Invocation.Builder accept(final String... mediaTypes) { + return null; + } + + @Override + public Invocation.Builder accept(final MediaType... mediaTypes) { + return null; + } + + @Override + public Invocation.Builder acceptLanguage(final Locale... locales) { + return null; + } + + @Override + public Invocation.Builder acceptLanguage(final String... locales) { + return null; + } + + @Override + public Invocation.Builder acceptEncoding(final String... encodings) { + return null; + } + + @Override + public Invocation.Builder cookie(final Cookie cookie) { + return null; + } + + @Override + public Invocation.Builder cookie(final String name, final String value) { + return null; + } + + @Override + public Invocation.Builder cacheControl(final CacheControl cacheControl) { + return null; + } + + @Override + public Invocation.Builder header(final String name, final Object value) { + return null; + } + + @Override + public Invocation.Builder headers(final MultivaluedMap headers) { + return null; + } + + @Override + public Invocation.Builder property(final String name, final Object value) { + return null; + } + + @Override + public CompletionStageRxInvoker rx() { + return null; + } + + @Override + public T rx(final Class clazz) { + return null; + } + + @Override + public Response get() { + return null; + } + + @Override + public T get(final Class responseType) { + return null; + } + + @Override + public T get(final GenericType responseType) { + return null; + } + + @Override + public Response put(final Entity entity) { + return null; + } + + @Override + public T put(final Entity entity, final Class responseType) { + return null; + } + + @Override + public T put(final Entity entity, final GenericType responseType) { + return null; + } + + @Override + public Response post(final Entity entity) { + return null; + } + + @Override + public T post(final Entity entity, final Class responseType) { + return null; + } + + @Override + public T post(final Entity entity, final GenericType responseType) { + return null; + } + + @Override + public Response delete() { + return null; + } + + @Override + public T delete(final Class responseType) { + return null; + } + + @Override + public T delete(final GenericType responseType) { + return null; + } + + @Override + public Response head() { + return null; + } + + @Override + public Response options() { + return null; + } + + @Override + public T options(final Class responseType) { + return null; + } + + @Override + public T options(final GenericType responseType) { + return null; + } + + @Override + public Response trace() { + return null; + } + + @Override + public T trace(final Class responseType) { + return null; + } + + @Override + public T trace(final GenericType responseType) { + return null; + } + + @Override + public Response method(final String name) { + return null; + } + + @Override + public T method(final String name, final Class responseType) { + return null; + } + + @Override + public T method(final String name, final GenericType responseType) { + return null; + } + + @Override + public Response method(final String name, final Entity entity) { + return null; + } + + @Override + public T method(final String name, final Entity entity, final Class responseType) { + return null; + } + + @Override + public T method(final String name, final Entity entity, final GenericType responseType) { + return null; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/TargetMock.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/TargetMock.java new file mode 100644 index 000000000..eb04fb0e3 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/track/impl/mock/TargetMock.java @@ -0,0 +1,151 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; + +import java.net.URI; +import java.util.Map; + +/** + * {@link jakarta.ws.rs.client.WebTarget} mock object. Used to track configuration correctness in + * {@link ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker} without real target. + * + * @author Vyacheslav Rusakov + * @since 04.10.2025 + */ +@SuppressWarnings("PMD.TooManyMethods") +public class TargetMock implements WebTarget { + + // used to collect uri changes to be able to show the resulted url + private final UriBuilder uriBuilder = UriBuilder.newInstance(); + + @Override + public URI getUri() { + return uriBuilder.build(); + } + + @Override + public UriBuilder getUriBuilder() { + return uriBuilder; + } + + @Override + public WebTarget path(final String path) { + uriBuilder.path(path); + return this; + } + + @Override + public WebTarget resolveTemplate(final String name, final Object value) { + uriBuilder.resolveTemplate(name, value); + return null; + } + + @Override + public WebTarget resolveTemplate(final String name, final Object value, final boolean encodeSlashInPath) { + uriBuilder.resolveTemplate(name, value, encodeSlashInPath); + return null; + } + + @Override + public WebTarget resolveTemplateFromEncoded(final String name, final Object value) { + uriBuilder.resolveTemplateFromEncoded(name, value); + return null; + } + + @Override + public WebTarget resolveTemplates(final Map templateValues) { + uriBuilder.resolveTemplates(templateValues); + return null; + } + + @Override + public WebTarget resolveTemplates(final Map templateValues, final boolean encodeSlashInPath) { + uriBuilder.resolveTemplates(templateValues, encodeSlashInPath); + return null; + } + + @Override + public WebTarget resolveTemplatesFromEncoded(final Map templateValues) { + uriBuilder.resolveTemplatesFromEncoded(templateValues); + return null; + } + + @Override + public WebTarget matrixParam(final String name, final Object... values) { + return null; + } + + @Override + public WebTarget queryParam(final String name, final Object... values) { + return null; + } + + @Override + public Invocation.Builder request() { + return new BuilderMock(); + } + + @Override + public Invocation.Builder request(final String... acceptedResponseTypes) { + return new BuilderMock(); + } + + @Override + public Invocation.Builder request(final MediaType... acceptedResponseTypes) { + return new BuilderMock(); + } + + @Override + public Configuration getConfiguration() { + return null; + } + + @Override + public WebTarget property(final String name, final Object value) { + return null; + } + + @Override + public WebTarget register(final Class componentClass) { + return null; + } + + @Override + public WebTarget register(final Class componentClass, final int priority) { + return null; + } + + @Override + public WebTarget register(final Class componentClass, final Class... contracts) { + return null; + } + + @Override + public WebTarget register(final Class componentClass, final Map, Integer> contracts) { + return null; + } + + @Override + public WebTarget register(final Object component) { + return null; + } + + @Override + public WebTarget register(final Object component, final int priority) { + return null; + } + + @Override + public WebTarget register(final Object component, final Class... contracts) { + return null; + } + + @Override + public WebTarget register(final Object component, final Map, Integer> contracts) { + return null; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/JerseyExceptionHandling.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/JerseyExceptionHandling.java new file mode 100644 index 000000000..bbd8ee64e --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/JerseyExceptionHandling.java @@ -0,0 +1,111 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotAcceptableException; +import jakarta.ws.rs.NotAllowedException; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.NotSupportedException; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.RedirectionException; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.ServiceUnavailableException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.ResponseProcessingException; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.client.internal.LocalizationMessages; + +/** + * Jersey provides additional exception handling for shortcut executions with response type (like + * {@code Invocation.Builder#method("GET", SomeClass.class)}). But it is impossible to re-use this logic + * when {@link jakarta.ws.rs.core.Response} is obtained. + *

        + * This class contain a copy of exceptions logic from + * {@link org.glassfish.jersey.client.JerseyInvocation#translate( + * org.glassfish.jersey.client.ClientResponse, org.glassfish.jersey.process.internal.RequestScope, Class)}. + * + * @author Vyacheslav Rusakov + * @since 19.09.2025 + */ +@SuppressWarnings({"checkstyle:ClassDataAbstractionCoupling", "checkstyle:CyclomaticComplexity"}) +public final class JerseyExceptionHandling { + + private JerseyExceptionHandling() { + } + + /** + * Throw custom exception for not successful response (not 2xx). The exception would be the same as with + * direct value call like {@code Invocation.Builder#method("GET", SomeClass.class)}. + * + * @param response response to check and theow exceptions. + * @throws ProcessingException for not successful request + */ + public static void throwIfNotSuccess(final Response response) throws ProcessingException { + if (response.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) { + final ProcessingException ex = convertToException(response); + if (ex.getCause() instanceof WebApplicationException) { + throw (WebApplicationException) ex.getCause(); + } else { + throw ex; + } + } + } + + /** + * Convert response to the exception. Assumed failed response object. + * + * @param response response + * @return specialized exception (according to response status) + */ + public static ProcessingException convertToException(final Response response) { + final int statusCode = response.getStatus(); + + try { + // Buffer and close entity input stream (if any) to prevent + // leaking connections (see JERSEY-2157). + response.bufferEntity(); + + final WebApplicationException webAppException; + final Response.Status status = Response.Status.fromStatusCode(statusCode); + + if (status == null) { + final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); + webAppException = createExceptionForFamily(response, statusFamily); + } else { + webAppException = switch (status) { + case BAD_REQUEST -> new BadRequestException(response); + case UNAUTHORIZED -> new NotAuthorizedException(response); + case FORBIDDEN -> new ForbiddenException(response); + case NOT_FOUND -> new NotFoundException(response); + case METHOD_NOT_ALLOWED -> new NotAllowedException(response); + case NOT_ACCEPTABLE -> new NotAcceptableException(response); + case UNSUPPORTED_MEDIA_TYPE -> new NotSupportedException(response); + case INTERNAL_SERVER_ERROR -> new InternalServerErrorException(response); + case SERVICE_UNAVAILABLE -> new ServiceUnavailableException(response); + default -> { + final Response.Status.Family statusFamily = response.getStatusInfo().getFamily(); + yield createExceptionForFamily(response, statusFamily); + } + }; + } + + return new ResponseProcessingException(response, webAppException); + } catch (final Throwable t) { + return new ResponseProcessingException(response, + LocalizationMessages.RESPONSE_TO_EXCEPTION_CONVERSION_FAILED(), t); + } + } + + private static WebApplicationException createExceptionForFamily(final Response response, + final Response.Status.Family statusFamily) { + return switch (statusFamily) { + case REDIRECTION -> new RedirectionException(response); + case CLIENT_ERROR -> new ClientErrorException(response); + case SERVER_ERROR -> new ServerErrorException(response); + default -> new WebApplicationException(response); + }; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/RequestModifierSource.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/RequestModifierSource.java new file mode 100644 index 000000000..fb2db0fa9 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/RequestModifierSource.java @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import com.google.common.collect.ImmutableList; +import ru.vyarus.dropwizard.guice.module.installer.util.StackUtils; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientDefaults; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; + +import java.util.List; + +/** + * Utility to detect configuration source. + * + * @author Vyacheslav Rusakov + * @since 03.10.2025 + */ +public final class RequestModifierSource { + + private static final List> INFRA = ImmutableList.of( + TestRequestConfig.class, + TestClientRequestBuilder.class, + TestClientDefaults.class, + TestClient.class, + ResourceClient.class, + RequestModifierSource.class + ); + + private RequestModifierSource() { + } + + /** + * @return test client calling source + */ + public static String getSource() { + return StackUtils.getCallerSource(INFRA); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestClientResponseCleanup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestClientResponseCleanup.java new file mode 100644 index 000000000..af917fe06 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestClientResponseCleanup.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.core.Response; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds references to responses, wrapped with + * {@link ru.vyarus.dropwizard.guice.test.client.builder.TestClientResponse} because they might be not consumed + * and so not closed. This holder aggregates all such requests to close them after a test. + *

        + * The holder itself is stored under the guicey configuration state. On state shutdown (after the test) close method + * would be called automatically, closing all stale resources. + *

        + * Of course, this mechanism is not a guarantee for all cases when generic test methods used instead of guicey test + * extensions. + * + * @author Vyacheslav Rusakov + * @since 16.09.2025 + */ +public class TestClientResponseCleanup implements AutoCloseable { + + private final List responses = new ArrayList<>(); + private boolean closed; + + /** + * Register a response to clean up after the test. + * + * @param response response to cleanup + */ + public void add(final Response response) { + // should be unreachable because client is closed after test and so it would be impossible to call resource + // after the application shutdown + Preconditions.checkState(!closed, "Application already closed"); + // just in case, cleanup references on each new addition (for cases when state shutdown never called) + responses.removeIf(Response::isClosed); + responses.add(response); + } + + @Override + public void close() throws Exception { + responses.forEach(Response::close); + responses.clear(); + closed = true; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestRequestConfigPrinter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestRequestConfigPrinter.java new file mode 100644 index 000000000..e00075d73 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/TestRequestConfigPrinter.java @@ -0,0 +1,111 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import jakarta.ws.rs.core.CacheControl; +import org.glassfish.jersey.message.internal.CacheControlProvider; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; +import ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue; + +import java.text.DateFormat; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +/** + * Utility to print configuration to string. + * + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +public final class TestRequestConfigPrinter { + + private TestRequestConfigPrinter() { + } + + /** + * @param config request config + * @return string representation for provided configuration + */ + public static String print(final TestRequestConfig config) { + final StringBuilder sb = new StringBuilder(1000); + + if (config.hasConfiguration()) { + printMap(sb, "Path params", config.getConfiguredPathParamsSource()); + printMap(sb, "Query params", config.getConfiguredQueryParamsSource()); + printMap(sb, "Matrix params", config.getConfiguredMatrixParamsSource()); + printMap(sb, "Headers", config.getConfiguredHeadersSource()); + printMap(sb, "Cookies", config.getConfiguredCookiesSource(), true); + printMap(sb, "Properties", config.getConfiguredPropertiesSource()); + printClasses(sb, "Extensions", config.getConfiguredExtensionsSource().values()); + printStrings(sb, "Accept", config.getConfiguredAcceptsSource()); + printStrings(sb, "Language", config.getConfiguredLanguagesSource()); + printStrings(sb, "Encoding", config.getConfiguredEncodingsSource()); + printClasses(sb, "Path modifiers", config.getConfiguredPathModifiersSource()); + printClasses(sb, "Request modifiers", config.getConfiguredRequestModifiersSource()); + + final SourceAwareValue cacheControl = config.getConfiguredCacheControlSource(); + if (cacheControl != null) { + title(sb, "Cache"); + item(sb, new CacheControlProvider().toString(cacheControl.get()), cacheControl.getSource()); + } + final SourceAwareValue dateFormatter = config.getConfiguredFormDateFormatterSource(); + if (dateFormatter != null) { + title(sb, "Custom Date (java.util) formatter"); + item(sb, dateFormatter.get().getClass().getSimpleName(), dateFormatter.getSource()); + } + final SourceAwareValue dateTimeFormatter = config + .getConfiguredFormDateTimeFormatterSource(); + if (dateTimeFormatter != null) { + title(sb, "Custom Date (java.time) formatter"); + item(sb, dateTimeFormatter.get().getClass().getSimpleName(), dateTimeFormatter.getSource()); + } + } else { + sb.append("\n\tNo configurations\n"); + } + + return sb.toString(); + } + + private static void printMap(final StringBuilder res, final String title, + final Map> params) { + printMap(res, title, params, false); + } + private static void printMap(final StringBuilder res, final String title, + final Map> params, final boolean skipKey) { + if (!params.isEmpty()) { + title(res, title); + params.forEach((s, o) -> + item(res, (skipKey ? "" : (s + "=")) + o.get(), o.getSource())); + } + } + + private static void printStrings(final StringBuilder res, final String title, + final SourceAwareValue strings) { + if (strings != null) { + title(res, title); + Arrays.stream(strings.get()).forEach(s -> item(res, s, strings.getSource())); + } + } + + private static void printClasses( + final StringBuilder res, final String title, final Collection> objects) { + if (!objects.isEmpty()) { + title(res, title); + objects.forEach(s -> { + final Object o = s.get(); + final Class aClass = (Class) (o instanceof Class ? o : o.getClass()); + final String name = (aClass.isAnonymousClass() || aClass.isSynthetic()) + ? "" : aClass.getSimpleName(); + item(res, name, s.getSource()); + }); + } + } + + private static void title(final StringBuilder res, final String title) { + res.append("\n\t").append(title).append(":\n"); + } + + private static void item(final StringBuilder res, final String item, final String source) { + res.append("\t\t").append(String.format("%-40s %s", item, source)).append('\n'); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/VoidBodyReader.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/VoidBodyReader.java new file mode 100644 index 000000000..9ea2c6bed --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/VoidBodyReader.java @@ -0,0 +1,45 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.Provider; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * In theory, {@code response.readEntity(Void.class)} should completely ignore response content (if present), but, + * actually jersey throws no mapper found error. This mapper should be used as a workaround: to completely ignore + * the response body. + * + * @author Vyacheslav Rusakov + * @since 17.09.2025 + * @see ru.vyarus.dropwizard.guice.test.client.builder.TestClientRequestBuilder#noBodyMappingForVoid() + */ +@Provider +@Consumes +public class VoidBodyReader implements MessageBodyReader { + + @Override + public boolean isReadable(final Class type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType) { + return type.equals(Void.class); + } + + @Override + public Void readFrom(final Class type, + final Type genericType, + final Annotation[] annotations, + final MediaType mediaType, + final MultivaluedMap httpHeaders, + final InputStream entityStream) throws IOException, WebApplicationException { + return null; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/FormParamsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/FormParamsSupport.java new file mode 100644 index 000000000..244ce8154 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/FormParamsSupport.java @@ -0,0 +1,116 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util.conf; + +import com.fasterxml.jackson.databind.util.StdDateFormat; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Array; +import java.text.DateFormat; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +/** + * Convers form parameters into strings. With a special support for dates conversion. + * + * @author Vyacheslav Rusakov + * @since 15.09.2025 + */ +public final class FormParamsSupport { + + /** + * Default java.util dates formatter. + */ + // StdDateFormat is thread safe + @SuppressFBWarnings("STCAL_STATIC_SIMPLE_DATE_FORMAT_INSTANCE") + public static final DateFormat DEFAULT_DATE_FORMAT = new StdDateFormat() + .withTimeZone(TimeZone.getTimeZone("UTC")) + .withColonInTimeZone(true); + + /** + * Default java.time dates formatter. + */ + public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + private FormParamsSupport() { + } + + /** + * Format the given parameter object into string. + *

        + * String conversion specifics: + *

          + *
        • Date fields string conversion could be customized with date formatters (one for java.util and other for + * java.time api).
        • + *
        • Null values converted to ""
        • + *
        • Collections and arrays converted to a comma-separated string of values (converted to string)
        • + *
        • By default, call toString on provided object
        • + *
        + *

        + * To customize date fields conversion use + * {@link #parameterToString(Object, java.text.DateFormat, java.time.format.DateTimeFormatter)}. + * + * @param param Object + * @return Object in string format + */ + public static String parameterToString(final Object param) { + return parameterToString(param, null, null); + } + + /** + * Format the given parameter object into string. + *

        + * String conversion specifics: + *

          + *
        • Date fields string conversion could be customized with date formatters (one for java.util and other for + * java.time api).
        • + *
        • Null values converted to ""
        • + *
        • Collections and arrays converted to a comma-separated string of values (converted to string)
        • + *
        • By default, call toString on provided object
        • + *
        + * + * @param param Object + * @param dateFormat java util date formatter + * @param dateTimeFormat java time date formatter + * @return Object in string format + */ + @SuppressWarnings("checkstyle:ReturnCount") + public static String parameterToString(final Object param, + final @Nullable DateFormat dateFormat, + final @Nullable DateTimeFormatter dateTimeFormat) { + final DateFormat format1 = dateFormat == null ? DEFAULT_DATE_FORMAT : dateFormat; + final DateTimeFormatter format2 = dateTimeFormat == null ? DEFAULT_DATE_TIME_FORMAT : dateTimeFormat; + + if (param == null) { + return ""; + // date, timestamp + } else if (param instanceof Date) { + return format1.format((Date) param); + // localdate, offsetdatetime etc. + } else if (param instanceof TemporalAccessor) { + return format2.format((TemporalAccessor) param); + } else if (param instanceof Collection) { + final StringBuilder b = new StringBuilder(); + ((Collection) param).forEach(val -> { + if (!b.isEmpty()) { + b.append(','); + } + b.append(parameterToString(val)); + }); + return b.toString(); + } else if (param.getClass().isArray()) { + final int length = Array.getLength(param); + final List params = new ArrayList<>(); + for (int i = 0; i < length; i++) { + params.add(Array.get(param, i)); + } + return parameterToString(params, format1, format2); + } else { + return String.valueOf(param); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/JerseyRequestConfigurer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/JerseyRequestConfigurer.java new file mode 100644 index 000000000..a6f87483a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/JerseyRequestConfigurer.java @@ -0,0 +1,103 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util.conf; + +import jakarta.ws.rs.client.WebTarget; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Helper utilities to apply jersey request configuration. + * + * @author Vyacheslav Rusakov + * @since 22.09.2025 + */ +public final class JerseyRequestConfigurer { + + private JerseyRequestConfigurer() { + } + + /** + * Supplier could return either single value or {@link java.lang.Iterable} or array to put multiple query + * parameters with tha same name. + * + * @param target original target + * @param queryParams query params map + * @return modified or the same target + */ + public static WebTarget applyQueryParams(final WebTarget target, + final Map> queryParams) { + return applyParams(target, queryParams, WebTarget::queryParam); + } + + /** + * Supplier could return either single value or {@link java.lang.Iterable} or array to put multiple matrix + * parameters with tha same name. + * + * @param target original target + * @param matrixParams query params map + * @return modified or the same target + */ + public static WebTarget applyMatrixParams(final WebTarget target, + final Map> matrixParams) { + return applyParams(target, matrixParams, WebTarget::matrixParam); + } + + /** + * Supplier could return either class or instance. Supplier could be null to register by type. + * + * @param target original target + * @param extensions jersey extensions to apply to client request + */ + public static void applyExtensions(final WebTarget target, + final Map, ? extends Supplier> extensions) { + for (Map.Entry, ? extends Supplier> entry : extensions.entrySet()) { + final Object ext = entry.getValue().get(); + if (ext instanceof Class) { + target.register((Class) ext); + } else { + target.register(ext); + } + } + } + + private static WebTarget applyParams(final WebTarget target, + final Map> params, + final ParamHandler applier) { + WebTarget out = target; + for (Map.Entry> entry : params.entrySet()) { + final Object value = entry.getValue().get(); + // special support for multiple values (in this case multiple query/matrix params must be applied) + final List multiple = new ArrayList<>(); + if (value instanceof Iterable) { + ((Iterable) value).forEach(multiple::add); + } else if (value.getClass().isArray()) { + multiple.addAll(Arrays.asList((Object[]) value)); + } else { + multiple.add(value); + } + final Object[] val = multiple.toArray(new Object[0]); + out = applier.apply(out, entry.getKey(), val); + } + return out; + } + + /** + * Parameter applier. + */ + @FunctionalInterface + public interface ParamHandler { + + /** + * Apply parameter. + * + * @param target web target + * @param name parameter name + * @param values values + * @return modified web target + */ + WebTarget apply(WebTarget target, String name, Object... values); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/MultipartSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/MultipartSupport.java new file mode 100644 index 000000000..53d3d2444 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/builder/util/conf/MultipartSupport.java @@ -0,0 +1,113 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util.conf; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.ContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Map; + +/** + * Build multipart form. Requires 'org.glassfish.jersey.media:jersey-media-multipart' dependency. + * + * @author Vyacheslav Rusakov + * @since 15.09.2025 + */ +public final class MultipartSupport { + + private MultipartSupport() { + } + + /** + * Build a multipart form entity from parameters. Any {@link org.glassfish.jersey.media.multipart.BodyPart} + * value used as-is (e.g. manually constructed {@link org.glassfish.jersey.media.multipart.FormDataBodyPart}). + * File could be specified as {@link java.io.File} or {@link java.io.InputStream}. All other values are converted + * to string. + *

        + * String conversion specifics: + *

          + *
        • Date fields string conversion could be customized with date formatters (one for java.util and other for + * java.time api).
        • + *
        • Null values converted to ""
        • + *
        • First level collection assumed to be a multi-value. Underlying collections are converted to strings.
        • + *
        • By default, call toString on a provided object
        • + *
        + * + * @param formParams form parameters + * @param dateFormat java.util dates formatter + * @param dateTimeFormat java.time dates formatter + * @return form multipart entity + */ + public static Entity buildMultipart(final Map formParams, + final @Nullable DateFormat dateFormat, + final @Nullable DateTimeFormatter dateTimeFormat) { + final FormDataMultiPart mp = new FormDataMultiPart(); + formParams.forEach((key, value) -> applyParam(mp, key, value, dateFormat, dateTimeFormat)); + return Entity.entity(mp, mp.getMediaType()); + } + + /** + * @param response response object + * @return file name from content-disposition header or null if header not found + */ + @Nullable + public static String readFilename(final Response response) { + final String header = response.getHeaderString(HttpHeader.CONTENT_DISPOSITION.toString()); + if (header != null) { + return readFilename(header); + } + return null; + } + + /** + * @param header content-disposition header + * @return filename from content-disposition header + */ + public static String readFilename(final String header) { + try { + final ContentDisposition contentDisposition = new ContentDisposition(header); + return contentDisposition.getFileName(true); + } catch (ParseException e) { + throw new IllegalStateException("Failed to parse " + HttpHeader.CONTENT_DISPOSITION + " header", e); + } + } + + private static void applyParam(final FormDataMultiPart mp, final String key, final Object value, + final @Nullable DateFormat dateFormat, + final @Nullable DateTimeFormatter dateTimeFormat) { + BodyPart bodyPart = null; + if (value instanceof BodyPart) { + bodyPart = (BodyPart) value; + } else if (value instanceof File) { + bodyPart = new FileDataBodyPart(key, (File) value); + } else if (value instanceof InputStream) { + bodyPart = new StreamDataBodyPart(key, (InputStream) value); + } + if (bodyPart != null) { + mp.bodyPart(bodyPart); + } else { + if (value instanceof Collection) { + for (Object val : (Collection) value) { + applyParam(mp, key, val, dateFormat, dateTimeFormat); + } + } else if (value.getClass().isArray()) { + for (Object val : (Object[]) value) { + applyParam(mp, key, val, dateFormat, dateTimeFormat); + } + } else { + mp.field(key, FormParamsSupport.parameterToString(value, dateFormat, dateTimeFormat)); + } + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/FileDownloadUtil.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/FileDownloadUtil.java new file mode 100644 index 000000000..5091db8b3 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/FileDownloadUtil.java @@ -0,0 +1,150 @@ +package ru.vyarus.dropwizard.guice.test.client.util; + +import jakarta.ws.rs.core.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.MultipartSupport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * File download from response utility. + * + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +public final class FileDownloadUtil { + + private FileDownloadUtil() { + } + + /** + * Download a file from the response. + *

        + * Limitation: does not support multipart responses. Use {@link #parseFileName(String)} + * and {@link #saveFile(String, InputStream, Path)} manually directly for multipart chunks. + * + * @param response response object + * @param dir target directory to save the file in + * @return downloaded ile + */ + public static Path download(final Response response, final Path dir) { + final String disposition = response.getHeaderString(HttpHeader.CONTENT_DISPOSITION.toString()); + String filename = "download_" + System.currentTimeMillis(); + if (disposition != null) { + final String name = readFilename(response); + if (name != null) { + filename = name; + + } + } + return saveFile(filename, response.readEntity(InputStream.class), dir); + } + + /** + * Save file from input stream to target directory. If a file with the same name already exists, an index will be + * added to the name. + * + * @param name file name (could be without extension) + * @param input file input stream + * @param dir target directory + * @return downloaded file + */ + public static Path saveFile(final String name, final InputStream input, final Path dir) { + final int idx = name.lastIndexOf('.'); + final String base = idx > 0 ? name.substring(0, idx) : name; + final String ext = idx > 0 ? name.substring(idx) : ""; + Path target = dir.resolve(name); + int cnt = 0; + while (target.toFile().exists()) { + target = dir.resolve(base + "(" + (++cnt) + ")" + ext); + } + + try { + Files.copy(input, target); + return target; + } catch (IOException e) { + throw new IllegalStateException("Failed to download file", e); + } + } + + /** + * Use jersey header parser when the multipart jar is available. Otherwise, fallback to less accurate manual + * parsing. + * + * @param response response object to read header from + * @return file name from content-disposition header or null if header not found + */ + @Nullable + public static String readFilename(final Response response) { + if (MultipartCheck.isEnabled()) { + // when multipart is available, use provided implementation ro parse header (more accurate) + return MultipartSupport.readFilename(response); + } + // when multipart id not available, parse header manually + return parseFileName(response); + } + + /** + * Less accurate version of {@link #readFilename(Response)} with simplified header parsing. Used when the multipart + * jar is not available (and so jersey native parsing can't be used). + * + * @param response response object to read header from + * @return file name from content-disposition header or null if header not found + */ + @Nullable + public static String parseFileName(final Response response) { + final String header = response.getHeaderString(HttpHeader.CONTENT_DISPOSITION.toString()); + if (header != null) { + return parseFileName(header); + } + return null; + } + + /** + * Less accurate version of {@link #readFilename(Response)} with simplified header parsing. Used when the multipart + * jar is not available (and so jersey native parsing can't be used). + * + * @param header content-disposition header + * @return parsed file name + */ + public static String parseFileName(final String header) { + String filename = null; + for (String part : header.split(";")) { + String chunk = part.trim(); + if (chunk.startsWith("filename=")) { + filename = unquote(chunk.substring("filename=".length()).trim()); + } + if (chunk.startsWith("filename*=")) { + chunk = unquote(chunk.substring("filename*=".length()).trim()); + if (chunk.contains("''")) { + final String[] parts = chunk.split("''"); + final String encoding = parts[0]; + try { + filename = URLDecoder.decode(parts[1], encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Failed to decode file name: " + parts[1], e); + } + } + break; + } + } + return filename; + } + + private static String unquote(final String value) { + String res = value; + if (res.contains("\"")) { + res = res.substring(1); + } + if (res.endsWith("\"")) { + res = res.substring(0, res.length() - 1); + } + return res; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/MultipartCheck.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/MultipartCheck.java new file mode 100644 index 000000000..fa242abb6 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/MultipartCheck.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.client.util; + +import com.google.common.base.Preconditions; + +import java.util.Optional; + +/** + * Utility to check multipart jar presence. + *

        + * Note: this can't be merged with {@link ru.vyarus.dropwizard.guice.test.client.builder.util.conf.MultipartSupport} as + * this class should not use any multipart classes directly. + * + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +public final class MultipartCheck { + + private static final String MULTIPART_FEATURE = "org.glassfish.jersey.media.multipart.MultiPartFeature"; + + private MultipartCheck() { + } + + /** + * @return true if the multipart jar is available + */ + public static boolean isEnabled() { + return getMultipartFeatureClass().isPresent(); + } + + /** + * @return multipart feature class or null if jar not available + */ + public static Optional> getMultipartFeatureClass() { + try { + return Optional.of(Class.forName(MULTIPART_FEATURE)); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + } + + /** + * @throws IllegalStateException if the multipart jar is not available + */ + public static void requireEnabled() { + Preconditions.checkState(isEnabled(), "Multipart feature is not enabled. Either add " + + "'io.dropwizard:dropwizard-forms' or 'org.glassfish.jersey.media:jersey-media-multipart'."); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/SourceAwareValue.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/SourceAwareValue.java new file mode 100644 index 000000000..88a26d299 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/client/util/SourceAwareValue.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.test.client.util; + +import java.util.function.Supplier; + +/** + * Value wrapper with a registration source (for debug output). + * + * @param value type + * @author Vyacheslav Rusakov + * @since 03.10.2025 + */ +public class SourceAwareValue implements Supplier { + private final Supplier supplier; + private final String source; + + /** + * Create source value. + * + * @param supplier value supplier + * @param source registration source (for debug output) + */ + public SourceAwareValue(final Supplier supplier, final String source) { + this.supplier = supplier; + this.source = source; + } + + @Override + public V get() { + return supplier.get(); + } + + /** + * @return registration source (for debug output) + */ + public String getSource() { + return source; + } + + @Override + public String toString() { + return "Value from " + source; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandResult.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandResult.java new file mode 100644 index 000000000..b9a636bdd --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandResult.java @@ -0,0 +1,167 @@ +package ru.vyarus.dropwizard.guice.test.cmd; + +import com.google.inject.Injector; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.annotation.Nullable; + +/** + * Dropwizard command execution result. Note that command execution never throws exceptions: instead, all thrown + * exceptions are intercepted and provided as the unsuccessful execution result. + *

        + * Depending on executed command type, some object might be null (injector would be available only for environment + * commands). + *

        + * Output contains both out and err streams (to simulate console view, when both streams are shown in console). + * Error output is also collected separately for validation (all these messages are present in common output too). + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +public class CommandResult { + private final boolean success; + private final Throwable exception; + private final String output; + private final String errorOutput; + private final Command command; + private final Application application; + private final Bootstrap bootstrap; + private final C configuration; + private final Environment environment; + private final Injector injector; + + /** + * Command result. + * + * @param success success indicator + * @param exception exception instance + * @param output console output + * @param errorOutput console error output + * @param command command instance + * @param app application instance + * @param bootstrap bootstrap instance + * @param configuration configuration instance + * @param environment environment instance + * @param injector injector instance + */ + @SuppressWarnings("checkstyle:ParameterNumber") + public CommandResult(final boolean success, + final @Nullable Throwable exception, + final String output, + final String errorOutput, + final @Nullable Command command, + final Application app, + final Bootstrap bootstrap, + final @Nullable C configuration, + final @Nullable Environment environment, + final @Nullable Injector injector) { + this.success = success; + this.exception = exception; + this.output = output; + this.errorOutput = errorOutput; + this.command = command; + this.application = app; + this.bootstrap = bootstrap; + this.configuration = configuration; + this.environment = environment; + this.injector = injector; + } + + /** + * @return true for successful execution, false is exception was thrown + */ + public boolean isSuccess() { + return success; + } + + /** + * @return exception, thrown during command execution (or null for successful execution) + */ + @Nullable + public Throwable getException() { + return exception; + } + + /** + * Note that default command + * {@link Command#onError(io.dropwizard.core.cli.Cli, net.sourceforge.argparse4j.inf.Namespace, Throwable)} + * implementation simply prints error stack trace to console, so output would contain this trace. + * + * @return console output (together with error stream to get output exactly as it would be in console) or + * empty string if no output + * @see #getErrorOutput() for error output only + */ + public String getOutput() { + return output; + } + + + /** + * Note that default command + * {@link Command#onError(io.dropwizard.core.cli.Cli, net.sourceforge.argparse4j.inf.Namespace, Throwable)} + * implementation simply prints error stack trace to console, so output would contain this trace. + * + * @return error output or empty string + */ + public String getErrorOutput() { + return errorOutput; + } + + /** + * Could be null only if incorrect command name was specified (in this case help message would be shown + * by dropwizard instead of execution). In all other cases command should not be null (it is searched manually + * in the bootstrap object before execution). + * + * @return command instance, used for execution + */ + @Nullable + public Command getCommand() { + return command; + } + + /** + * @return application instance used for execution + */ + public Application getApplication() { + return application; + } + + /** + * @return bootstrap object used for execution + */ + public Bootstrap getBootstrap() { + return bootstrap; + } + + /** + * Could be null for command without configuration and in case of configuration parse errors. + * + * @return configuration instance used or null + */ + @Nullable + public C getConfiguration() { + return configuration; + } + + /** + * @return environment instance or null (for non-environment commands or startup error) + */ + @Nullable + public Environment getEnvironment() { + return environment; + } + + /** + * Note: injector created only for {@link io.dropwizard.core.cli.EnvironmentCommand}. + * + * @return injector instance or null (for non-environment commands or due to injector startup errors) + */ + @Nullable + public Injector getInjector() { + return injector; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandRunBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandRunBuilder.java new file mode 100644 index 000000000..7de022b2a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandRunBuilder.java @@ -0,0 +1,145 @@ +package ru.vyarus.dropwizard.guice.test.cmd; + +import com.google.common.base.MoreObjects; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import ru.vyarus.dropwizard.guice.test.builder.BaseBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for {@link ru.vyarus.dropwizard.guice.test.cmd.CommandTestSupport}. Provides almost the same methods + * as application support builder ({@link ru.vyarus.dropwizard.guice.test.builder.TestSupportBuilder}). + *

        + * Use {@link ru.vyarus.dropwizard.guice.test.TestSupport#buildCommandRunner(Class)} for builder creation. + *

        + * Builder is not supposed to be used for multiple runs: registered hooks will be applied only once. This limitation + * is not possible to avoid because builder could be used for support objects creation, which are not aware of + * hooks. So hooks could be registered globally only in time of addition to the builder. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +public class CommandRunBuilder extends BaseBuilder> { + + private String[] inputs; + private final List> listeners = new ArrayList<>(); + + /** + * Create builder. + * + * @param app application class + */ + public CommandRunBuilder(final Class> app) { + super(app); + } + + /** + * The amount af answers should not be less than provided answers count. + * + * @param inputs answers for command console questions + * @return builder instance for chained calls + */ + public CommandRunBuilder consoleInputs(final String... inputs) { + this.inputs = inputs; + return this; + } + + /** + * Simple listener to setup and cleanup something before and after command execution. + * + * @param listener listener + * @return builder instance for chained calls + */ + public CommandRunBuilder listen(final CommandListener listener) { + this.listeners.add(listener); + return this; + } + + /** + * Shortcut for {@code run("server")} to start application. Should be used to test application startup errors. + * Error would be thrown if the application started successfully (to avoid frozen test). + * + * @return execution result + */ + public CommandResult runApp() { + return run("server"); + } + + /** + * Execute dropwizard command. Could be used to execute any command. The only difference with the usual usage + * is that configuration file should not be declared (as second argument). Config file could be specified - + * it would not lead to error (if the config file path was not declared in builder also). + *

        + * Execution never throws an exception! Any appeared exception would be returned inside an unsuccessful result. + *

        + * As it is not possible to run any callback in time of command execution - all runtime objects are provided + * inside the result for inspection (some objects could be null, depending on a command type). + *

        + * All command output would be available in the result. Also, output is streamed to console (to indicate the + * exact app froze point, if command hangs). Error stream is also available separately to simplify error + * check. + * + * @param args command execution arguments (without configuration file) + * @return command execution result + */ + public CommandResult run(final String... args) { + for (CommandListener listener : listeners) { + listener.setup(args); + } + + final CommandResult result = inputs == null ? build().run(args) + : build().run(inputs, args); + + for (CommandListener listener : listeners) { + listener.cleanup(result); + } + return result; + } + + private CommandTestSupport build() { + final CommandTestSupport support; + if (configObject != null) { + if (configPath != null || !configOverrides.isEmpty() || configSourceProvider != null) { + throw new IllegalStateException("Configuration object can't be used together with yaml configuration"); + } + support = new CommandTestSupport<>(app, configObject); + } else { + final String prefix = MoreObjects.firstNonNull(propertyPrefix, "dw."); + support = new CommandTestSupport<>(app, configPath, configSourceProvider, + prefix, prepareOverrides(prefix)); + } + support.configModifiers(modifiers); + + return support; + } + + /** + * Command execution listener. Could be used to apply some additional initialization and clean for + * command execution. + * + * @param configuration type + */ + public interface CommandListener { + + /** + * Called before command execution. + * + * @param args run arguments (without added configuration file) - exactly as specified in test + */ + default void setup(final String... args) { + // empty + } + + /** + * Called after command execution (even if execution fails). + * + * @param result command execution result + */ + default void cleanup(final CommandResult result) { + // empty + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandTestSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandTestSupport.java new file mode 100644 index 000000000..6a19d76c4 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/cmd/CommandTestSupport.java @@ -0,0 +1,436 @@ +package ru.vyarus.dropwizard.guice.test.cmd; + +import com.google.common.base.Preconditions; +import com.google.inject.Injector; +import io.dropwizard.configuration.ConfigurationException; +import io.dropwizard.configuration.ConfigurationFactory; +import io.dropwizard.configuration.ConfigurationSourceProvider; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.cli.CheckCommand; +import io.dropwizard.core.cli.Cli; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.cli.ServerCommand; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.logging.common.LoggingUtil; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.POJOConfigurationFactory; +import io.dropwizard.util.JarLocation; +import jakarta.annotation.Nullable; +import org.slf4j.Logger; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.io.EchoStream; +import ru.vyarus.dropwizard.guice.test.util.io.SystemInMock; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Test helper for running (any) commands. The class is almost similar to + * {@link io.dropwizard.testing.DropwizardTestSupport}, but differs in a way command is executed: this class + * use {@link io.dropwizard.core.cli.Cli} which selects exactly the same command as in real use. Also, command + * execution is a one-shot operation and so all validations could be performed only after command execution + * (and not in the middle, as with usual application tests). That's why the resulting object contains all + * objects used during execution - there is no other way to access them. + *

        + * Supposed to be used through builder: {@link ru.vyarus.dropwizard.guice.test.TestSupport#buildCommandRunner(Class)}. + *

        + * All types of dropwizard commands are supported, but depending on the command type, some objects in result would be + * null. Note that guicey could only be used with {@link io.dropwizard.core.cli.EnvironmentCommand} - for other + * commands it would be simply ignored (because dropwizard would not call bundle's run method). + *

        + * Configuration support is the same as in dropwizard support: config object or configuration file with config + * overrides might be used. When a configuration file is used, it would be automatically added to called command + * (as a second argument). + *

        + * System in, err and out streams are overridden. To test commands with used input, input strings must be declared + * before command run. The resulting object would contain complete command output. + *

        + * Execution never throws an error: in case of exception, it would be provided inside the resulting object. + *

        + * Class is also suitable for application server startup errors check: instead of system exit, it will provide + * exception in the resulted object. If exception does not appear during startup, test would be failed to prevent + * infinite run (indicating unexpected successful run). + *

        + * Command tests can't be executed in parallel (due to system io overrides)! For junit 5 use + * {@code @Execution(SAME_THREAD)} on test class to prevent concurrent execution. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +@SuppressWarnings({"PMD.ExcessiveImports", "checkstyle:ClassFanOutComplexity", + "checkstyle:ClassDataAbstractionCoupling"}) +public class CommandTestSupport { + + private static final PrintStream SYS_OUT = System.out; + private static final PrintStream SYS_ERR = System.err; + private static final InputStream SYS_IN = System.in; + + /** + * Application class. + */ + protected final Class> applicationClass; + /** + * Configuration file path. + */ + protected final String configPath; + /** + * Configuration source provider. + */ + protected final ConfigurationSourceProvider configSourceProvider; + /** + * Configuration overrides. + */ + protected final Set configOverrides; + /** + * Configuration modifiers. + */ + protected final List> modifiers = new ArrayList<>(); + /** + * Configuration overrides prefix. + */ + protected final String customPropertyPrefix; + + /** + * Flag that indicates whether instance was constructed with an explicit + * Configuration object or not; handling of the two cases differ. + * Needed because state of {@link #configuration} changes during lifecycle. + */ + protected final boolean explicitConfig; + /** + * Configuration instance (used instead of file). + */ + protected C configuration; + /** + * Environment instance. + */ + protected Environment environment; + /** + * Injector instance. + */ + protected Injector injector; + /** + * Application instance. + */ + protected Application application; + /** + * Bootstrap instance. + */ + protected Bootstrap bootstrap; + + // print all output to real console (to track execution and visually see hangs) + private final EchoStream stdOut = new EchoStream(SYS_OUT); + // err stream is merged in output and collected separately, just in case + private final EchoStream stdErr = new EchoStream(stdOut); + private final SystemInMock stdIn = new SystemInMock(); + private Cli cli; + + /** + * Create a support object. + * + * @param applicationClass application class + * @param configuration configuration instance + */ + public CommandTestSupport(final Class> applicationClass, final C configuration) { + this.applicationClass = applicationClass; + this.configPath = ""; + this.configSourceProvider = null; + this.configOverrides = Collections.emptySet(); + this.customPropertyPrefix = null; + this.configuration = configuration; + this.explicitConfig = true; + } + + /** + * Create a support object. + * + * @param applicationClass application class + * @param configPath configuration file path + * @param configSourceProvider configuration source provider + * @param customPropertyPrefix config overrides prefix + * @param configOverrides config overrides + */ + public CommandTestSupport(final Class> applicationClass, + final @Nullable String configPath, + final @Nullable ConfigurationSourceProvider configSourceProvider, + final @Nullable String customPropertyPrefix, + final ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = configPath; + this.configSourceProvider = configSourceProvider; + this.configOverrides = Optional.ofNullable(configOverrides) + .map(Set::of) + .orElse(Set.of()); + this.customPropertyPrefix = customPropertyPrefix; + this.explicitConfig = false; + } + + /** + * Register configuration modifiers. + * + * @param modifier configuration modifiers + * @return command support instance for chained calls + * @throws java.lang.IllegalStateException if called after application startup + */ + @SafeVarargs + public final CommandTestSupport configModifiers(final ConfigModifier... modifier) { + return configModifiers(Arrays.asList(modifier)); + } + + /** + * Register configuration modifiers. + * + * @param modifiers configuration modifiers + * @return command support instance for chained calls + * @throws java.lang.IllegalStateException if called after application startup + */ + public CommandTestSupport configModifiers(final List> modifiers) { + Preconditions.checkState(application == null, "Application is already created"); + this.modifiers.addAll(modifiers); + return this; + } + + /** + * Execute dropwizard command. Could be used to execute any command. The only difference with the usual usage + * is that configuration file should not be declared (as second argument). Config file could be specified - + * it would not lead to error (if the config file path was not declared in builder also). + *

        + * Execution never throws an exception! Any appeared exception would be returned inside an unsuccessful result. + *

        + * As it is not possible to run any callback in time of command execution - all runtime objects are provided + * inside the result for inspection (some objects could be null, depending on a command type). + *

        + * All command output would be available in the result. Also, output is streamed to console (to indicate the + * exact app froze point, if command hangs). Error stream is also available separately to simplify error + * check. + * + * @param args command execution arguments (without configuration file) + * @return command execution result + */ + public CommandResult run(final String... args) { + return run(null, args); + } + + /** + * Run for commands requiring console user input (in all other aspects is the same as {@link #run(String...)}). + *

        + * Error would be thrown if provided responses are not enough (on the first input request, not covered by mock + * data). + * + * @param input user input (should be the same (or more) then application would ask + * @param args command run arguments + * @return command execution result + */ + public CommandResult run(final @Nullable String[] input, final String... args) { + if (input != null) { + stdIn.provideText(input); + } + + final String[] params = insertConfigFile(args); + // visually separate command output to simplify test output reading + SYS_OUT.println("\n\n" + applicationClass.getSimpleName() + " COMMAND: " + + String.join(" ", params) + (input == null ? "" + : (" (with " + input.length + " inputs)"))); + SYS_OUT.println("-------------------------------------------------------------------------------------\n"); + + Throwable err = null; + try { + // fail in case of successful server startup (this should be used ONLY to test startup fails) + before(params.length > 0 && "server".equals(params[0])); + } catch (Exception ex) { + err = ex; + } + + final Application app = application; + // if command would not be found, dropwizard would fail on run so no need to throw error here + Command run = null; + if (params.length > 0) { + final String name = params[0]; + for (Command cmd : bootstrap.getCommands()) { + if (cmd.getName().equals(name)) { + run = cmd; + break; + } + } + } + + if (err == null) { + // never throw error + err = cli.run(params).orElse(null); + } + + reset(); + return new CommandResult<>(err == null, err, stdOut.toString(), stdErr.toString(), + run, app, bootstrap, configuration, environment, injector); + } + + /** + * Prepare command execution. + * + * @param preventServerStart true to avoid server start + * @throws Exception on error + */ + protected void before(final boolean preventServerStart) throws Exception { + applyConfigOverrides(); + + // Redirect stdout and stderr to our byte streams + System.setOut(new PrintStream(stdOut, false, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(stdErr, false, StandardCharsets.UTF_8)); + System.setIn(stdIn); + + application = InstanceUtils.create(applicationClass); + bootstrap = new Bootstrap<>(application); + initializeBootstrap(preventServerStart); + + // Build what'll run the command and interpret arguments + cli = new Cli(new DummyJarLocation(), bootstrap, System.out, System.err); + } + + /** + * Reset system state after execution. + */ + protected void reset() { + resetConfigOverrides(); + // Don't leak logging appenders into other test cases + if (configuration != null) { + configuration.getLoggingFactory().reset(); + } else { + LoggingUtil.getLoggerContext().getLogger(Logger.ROOT_LOGGER_NAME).detachAndStopAllAppenders(); + } + application = null; + + System.setOut(SYS_OUT); + System.setErr(SYS_ERR); + System.setIn(SYS_IN); + } + + private void applyConfigOverrides() { + configOverrides.forEach(ConfigOverride::addToSystemProperties); + } + + private void resetConfigOverrides() { + configOverrides.forEach(ConfigOverride::removeFromSystemProperties); + } + + private void initializeBootstrap(final boolean preventServerStart) { + // register default command (see io.dropwizard.core.Application.addDefaultCommands) + bootstrap.addCommand(new ServerCommand<>(application)); + bootstrap.addCommand(new CheckCommand<>(application)); + + application.initialize(bootstrap); + // important to put it after all other bundles to be able to resolve injector + bootstrap.addBundle(new ConfiguredBundle<>() { + @Override + public void run(final C configuration, final Environment environment) { + CommandTestSupport.this.environment = environment; + // it would be impossible to resolve injector reference after shutdown + CommandTestSupport.this.injector = InjectorLookup.getInjector(environment).orElse(null); + + if (preventServerStart) { + environment.lifecycle().addServerLifecycleListener(server -> { + throw new IllegalStateException( + "Application was expected to fail on startup, but successfully started instead"); + }); + } + } + }); + + if (configSourceProvider != null) { + bootstrap.setConfigurationSourceProvider(configSourceProvider); + } + + if (explicitConfig) { + // pojo factory does nothing - it's ok to run modifiers here + ConfigOverrideUtils.runModifiers(configuration, modifiers); + bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) -> + new POJOConfigurationFactory<>(configuration)); + } else if (customPropertyPrefix != null) { + final String prefix = customPropertyPrefix; + bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) -> + new ConfigInterceptor<>(new YamlConfigurationFactory<>(klass, validator, objectMapper, prefix), + // the only way to intercept command instance in case of ConfiguredCommand + c -> { + ConfigOverrideUtils.runModifiers(c, modifiers); + this.configuration = c; + })); + } + } + + private String[] insertConfigFile(final String... args) { + String[] params = args; + if (configPath != null && args.length > 0) { + params = new String[args.length + 1]; + params[0] = args[0]; + // have to include config path into command, becuase there is no wasy to embed it directly into + // namespace as in DropwizardTestSupport + params[1] = configPath; + if (args.length > 1) { + System.arraycopy(args, 2, params, 1, args.length); + } + } + return params; + } + + /** + * The only way to intercept configuration in all cases is to wrap the configuration factory + * (because bundle run method would be called only for environment commands). + * + * @param configuration type + */ + private static class ConfigInterceptor implements ConfigurationFactory { + private final ConfigurationFactory realFactory; + private final Consumer action; + + ConfigInterceptor(final ConfigurationFactory realFactory, final Consumer action) { + this.realFactory = realFactory; + this.action = action; + } + + @Override + public C build(final ConfigurationSourceProvider provider, final String path) + throws IOException, ConfigurationException { + final C res = realFactory.build(provider, path); + action.accept(res); + return res; + } + + @Override + public C build() throws IOException, ConfigurationException { + final C res = realFactory.build(); + action.accept(res); + return res; + } + } + + /** + * Fake jar locator. It is required only to correctly print version and jar name in help, but for + * command tests it is not important. + */ + private static class DummyJarLocation extends JarLocation { + + DummyJarLocation() { + super(CommandTestSupport.class); + } + + @Override + public Optional getVersion() { + return Optional.of("1.0.0"); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java similarity index 64% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java index c7889f9f3..e8c347413 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestDropwizardApp.java @@ -1,10 +1,13 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.extension.ExtendWith; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -25,7 +28,7 @@ * then declare extension in {@link org.junit.jupiter.api.extension.RegisterExtension} non-static field instead of * annotation. *

        - * Guice injections will work on test fields annotated with {@link javax.inject.Inject} or + * Guice injections will work on test fields annotated with {@link jakarta.inject.Inject} or * {@link com.google.inject.Inject} ({@link com.google.inject.Injector#injectMembers(Object)} applied on test instance). * Guice AOP will not work on test methods (because test itself is not created by guice). *

        @@ -77,9 +80,18 @@ * of annotation. * * @return list of overridden configuration values (may be used even without real configuration) + * @see #configModifiers() */ String[] configOverride() default {}; + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + * + * @return configuration modifiers + */ + Class>[] configModifiers() default {}; + /** * Hooks provide access to guice builder allowing complete customization of application context * in tests. @@ -147,11 +159,27 @@ */ Class[] setup() default {}; + + /** + * When test lifecycle is {@link org.junit.jupiter.api.TestInstance.Lifecycle#PER_CLASS} same test instance + * used for all test methods. By default, guicey would perform fields injection before each method because + * there might be prototype beans that must be refreshed for each test method. If you don't rely on + * prototypes, injections could be performed just once (for the first test method). + * + * @return true to inject guice beans once per test instance, false otherwise + */ + boolean injectOnce() default false; + /** * Enables debug output for extension: used setup objects, hooks and applied config overrides. Might be useful * for concurrent tests too because each message includes configuration prefix (exactly pointing to context test * or method). *

        + * Also, shows guicey extension time, so if you suspect that guicey spent too much time, use the debug option to + * be sure. Performance report is published after each "before each" phase and after "after all" to let you + * see how extension time increased with each test method (for non-static guicey extension (executed per method), + * performance printed after "before each" and "after each" because before/after all not available) + *

        * Configuration overrides are printed after application startup (but before the test) because overridden values * are resolved from system properties (applied by {@link io.dropwizard.testing.DropwizardTestSupport#before()}). * If application startup failed, no configuration overrides would be printed (because dropwizard would immediately @@ -164,4 +192,63 @@ * @return true to enable debug output, false otherwise */ boolean debug() default false; + + /** + * By default, a new application instance is started for each test. If you want to re-use the same application + * instance between several tests, then put extension declaration in BASE test class and enable the reuse option: + * all tests derived from this base class would use the same application instance. + *

        + * You may have multiple base classes with reusable application declaration (different test hierarchies) - in + * this case, multiple applications would be kept running during tests execution. + *

        + * All other extensions (without enabled re-use) will start new applications: take this into account to + * prevent port clashes with already started reusable apps. + *

        + * Reused application instance would be stopped after all tests execution. + * + * @return true to reuse application, false to start application for each test + */ + boolean reuseApplication() default false; + + /** + * Default extensions: {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest}. + *

        + * Disables service lookup for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup}. + *

        + * By default, these extensions enabled and this option could disable them (if there are problems with them or + * fields analysis took too much time). + * + * @return true to use default extensions + */ + boolean useDefaultExtensions() default true; + + /** + * Custom client factory for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object. Custom factory + * may be required in case when custom client configuration is required for test. + *

        + * Note: value is ignored when {@link #apacheClient()} set to true + * + * @return client factory class + */ + Class clientFactory() default DefaultTestClientFactory.class; + + /** + * Shortcut for {@link #clientFactory()} to configure + * {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory}. The default + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} supports only HTTP 1.1 methods and have + * problem with PATCH method usage on jdk > 16. + *

        + * Note: {@link #clientFactory()} value is ignored when set to true, + *

        + * Apache client is not set by default because of its problems with multipart: see + * jersey issue. + * + * @return true to use apache connection provider in jersey client + */ + boolean apacheClient() default false; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java similarity index 56% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java index 803cd62b1..cfc73fe2c 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/TestGuiceyApp.java @@ -1,11 +1,14 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.extension.ExtendWith; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -30,7 +33,7 @@ * then declare extension in {@link org.junit.jupiter.api.extension.RegisterExtension} non-static field instead of * annotation. *

        - * Guice injections will work on test fields annotated with {@link javax.inject.Inject} or + * Guice injections will work on test fields annotated with {@link jakarta.inject.Inject} or * {@link com.google.inject.Inject} ({@link com.google.inject.Injector#injectMembers(Object)} applied on test instance). * Guice AOP will not work on test methods (because test itself is not created by guice). *

        @@ -82,9 +85,18 @@ * of annotation. * * @return list of overridden configuration values (may be used even without real configuration) + * @see #configModifiers() */ String[] configOverride() default {}; + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + * + * @return configuration modifiers + */ + Class>[] configModifiers() default {}; + /** * Hooks provide access to guice builder allowing complete customization of application context * in tests. @@ -120,11 +132,26 @@ */ Class[] setup() default {}; + /** + * When test lifecycle is {@link org.junit.jupiter.api.TestInstance.Lifecycle#PER_CLASS} same test instance + * used for all test methods. By default, guicey would perform fields injection before each method because + * there might be prototype beans that must be refreshed for each test method. If you don't rely on + * prototypes, injections could be performed just once (for the first test method). + * + * @return true to inject guice beans once per test instance, false otherwise + */ + boolean injectOnce() default false; + /** * Enables debug output for extension: used setup objects, hooks and applied config overrides. Might be useful * for concurrent tests too because each message includes configuration prefix (exactly pointing to context test * or method). *

        + * Also, shows guicey extension time, so if you suspect that guicey spent too much time, use the debug option to + * be sure. Performance report is published after each "before each" phase and after "after all" to let you + * see how extension time increased with each test method (for non-static guicey extension (executed per method), + * performance printed after "before each" and "after each" because before/after all not available) + *

        * Configuration overrides are printed after application startup (but before the test) because overridden values * are resolved from system properties (applied by {@link io.dropwizard.testing.DropwizardTestSupport#before()}). * If application startup failed, no configuration overrides would be printed (because dropwizard would immediately @@ -137,4 +164,78 @@ * @return true to enable debug output, false otherwise */ boolean debug() default false; + + /** + * By default, a new application instance is started for each test. If you want to re-use the same application + * instance between several tests, then put extension declaration in BASE test class and enable the reuse option: + * all tests derived from this base class would use the same application instance. + *

        + * You may have multiple base classes with reusable application declaration (different test hierarchies) - in + * this case, multiple applications would be kept running during tests execution. + *

        + * All other extensions (without enabled re-use) will start new applications: take this into account to + * prevent port clashes with already started reusable apps. + *

        + * Reused application instance would be stopped after all tests execution. + * + * @return true to reuse application, false to start application for each test + */ + boolean reuseApplication() default false; + + /** + * Default extensions: {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest}. + *

        + * Disables service lookup for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup}. + *

        + * By default, these extensions enabled and this option could disable them (if there are problems with them or + * fields analysis took too much time). + * + * @return true to use default extensions + */ + boolean useDefaultExtensions() default true; + + /** + * Custom client factory for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object. Custom factory + * may be required in case when custom client configuration is required for test. + *

        + * Core test does not start dropwizard web services, but client still could be used to call external services. + *

        + * Note: value is ignored when {@link #apacheClient()} set to true + * + * @return client factory class + */ + Class clientFactory() default DefaultTestClientFactory.class; + + /** + * Shortcut for {@link #clientFactory()} to configure + * {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory}. The default + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} supports only HTTP 1.1 methods and have + * problem with PATCH method usage on jdk > 16. + *

        + * Note: {@link #clientFactory()} value is ignored when set to true, + *

        + * Apache client is not set by default because of its problems with multipart: see + * jersey issue. + * + * @return true to use apache connection provider in jersey client + */ + boolean apacheClient() default false; + + /** + * By default, guicey simulates {@link io.dropwizard.lifecycle.Managed} objects lifecycle. + *

        + * It might be required in test to avoid starting managed objects (especially all managed in application) because + * important (for test) services replaced with mocks (and no need to wait for the rest of the application). + *

        + * Note that {@link org.eclipse.jetty.util.component.LifeCycle} would still be supported as internal events rely on + * it (it is assumed that the application use only managed objects to initialize logic). + * + * @return true to simulate managed objects lifecycle, false to disable simulation + */ + boolean managedLifecycle() default true; } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/EnableSetup.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java new file mode 100644 index 000000000..8c9c95f78 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/ListenersSupport.java @@ -0,0 +1,131 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env; + +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.function.ThrowingConsumer; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Guicey test listeners support object. + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public class ListenersSupport { + + private final Set listeners = new LinkedHashSet<>(); + private final TestExtensionsTracker tracker; + + /** + * Create support. + * + * @param tracker tracker + */ + public ListenersSupport(final TestExtensionsTracker tracker) { + this.tracker = tracker; + } + + /** + * Register listener. + * + * @param listener listener + */ + public void addListener(final TestExecutionListener listener) { + listeners.add(Preconditions.checkNotNull(listener, "Listener must not be null")); + } + + /** + * Application starting. + * + * @param context junit context + */ + public void broadcastStarting(final ExtensionContext context) { + broadcast(listener -> listener.starting(new EventContext(context, tracker.debug))); + } + + /** + * Application started. + * + * @param context junit context + */ + public void broadcastStart(final ExtensionContext context) { + broadcast(listener -> listener.started(new EventContext(context, tracker.debug))); + } + + /** + * Before all test methods. + * + * @param context junit context + */ + public void broadcastBeforeAll(final ExtensionContext context) { + broadcast(listener -> listener.beforeAll(new EventContext(context, tracker.debug))); + } + + /** + * Before each test method. + * + * @param context junit context + */ + public void broadcastBefore(final ExtensionContext context) { + broadcast(listener -> listener.beforeEach(new EventContext(context, tracker.debug))); + } + + /** + * After each test method. + * + * @param context junit context + */ + public void broadcastAfter(final ExtensionContext context) { + broadcast(listener -> listener.afterEach(new EventContext(context, tracker.debug))); + } + + /** + * After all test methods. + * + * @param context junit context + */ + public void broadcastAfterAll(final ExtensionContext context) { + broadcast(listener -> listener.afterAll(new EventContext(context, tracker.debug))); + } + + /** + * Application stopping. + * + * @param context junit context + */ + public void broadcastStopping(final ExtensionContext context) { + broadcast(listener -> listener.stopping(new EventContext(context, tracker.debug))); + } + + /** + * Application stopped. + * + * @param context junit context + */ + public void broadcastStop(final ExtensionContext context) { + broadcast(listener -> listener.stopped(new EventContext(context, tracker.debug))); + } + + private void broadcast(final ThrowingConsumer action) { + if (!listeners.isEmpty()) { + final Stopwatch timer = Stopwatch.createStarted(); + listeners.forEach(l -> { + try { + action.accept(l); + } catch (Throwable ex) { + Throwables.throwIfUnchecked(ex); + throw new IllegalStateException("Failed to execute listener", ex); + } + }); + tracker.performanceTrack(GuiceyTestTime.TestListeners, timer.elapsed()); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java similarity index 55% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java index bd28a2bbe..874ba3a45 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestEnvironmentSetup.java @@ -2,14 +2,21 @@ /** * Extension for guicey junit 5 test extensions ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp} - * and {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}). Called before test application - * execution. Useful for management of additional environment objects like embedded database and + * and {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp}). Called before test support object and + * test application creation. Provides additional abilities to configure test. + *

        + * Useful for management of additional environment objects like embedded database and * overriding test application configuration. Consider this as a simpler option to writing custom junit extensions. *

        * If you need to take action after test execution (e.g. shutdown database) then return {@link java.lang.AutoCloseable} * or {@link org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource} object, and it would be * closed automatically. *

        + * If auto close is not enough, use + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension#listen( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} listener for reacting on exact test + * phases (or lambda-based listener methods: on*). + *

        * The same could be achieved with an additional junit 5 extensions, but it might be harder to properly synchronize * lifecycles (extensions order would be important). Environment support assumed to be a simpler alternative. *

        @@ -20,6 +27,18 @@ *

        * To avoid confusion with guicey hooks: setup object required to prepare test environment before test (and apply * required configurations) whereas hooks is a general mechanism for application customization (not only in tests). + * Setup objects do not duplicate all hook methods, instead a new hook could be registered from the setup object + * (e.g., if you need extension context access in hook - you should register a setup object and then create hook + * (inside it) providing entire junit context or just some stored values. + *

        + * For complex extensions it is recommended to implement hook + * ({@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook}) and/or listener + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}) interfaces directly + * (and register them as {@code .hooks(this).listen(this)}). + *

        + * Environment setup could be loaded with {@link java.util.ServiceLoader} to avoid manual registration: add + * {@code META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} file with one or more + * implementation classes (one per line). * * @author Vyacheslav Rusakov * @since 12.05.2022 @@ -28,9 +47,10 @@ public interface TestEnvironmentSetup { /** - * Called before test application startup under junit "before all" phase. Assumed to be used for starting - * additional test objects (like embedded database) and application configuration (configuration overrides). - * Provided object allow you to provide direct configuration overrides (e.g. to override database credentials). + * Called before test application startup under junit "before all" phase or "before each" (depends on extension + * registration). Assumed to be used for starting additional test objects (like embedded database) and application + * configuration (configuration overrides).Provided object allow you to provide direct configuration overrides + * (e.g. to override database credentials). *

        * For simplicity, any non closable returned object simply ignored. This was done to simplify lambas usage: * {@code TestEnvironmentSetup env = ext -> ext.configOverrides("foo:1")} - here configuration object @@ -40,6 +60,8 @@ public interface TestEnvironmentSetup { * @return {@link java.lang.AutoCloseable} or * {@link org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource} if something needs to be * shut down after test, any other object would be ignored (including null) + * + * @throws java.lang.Exception on error (to simplify usage) */ - Object setup(TestExtension extension); + Object setup(TestExtension extension) throws Exception; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java new file mode 100644 index 000000000..f40058bb1 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java @@ -0,0 +1,247 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env; + +import io.dropwizard.core.Configuration; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.ListenerEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.TestExecutionListenerLambdaAdapter; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.TestFieldUtils; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Configuration object for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} objects. + * + * @author Vyacheslav Rusakov + * @since 15.05.2022 + */ +// no configuration parameter because it could brake existing code (without declared generic types work incorrectly) +public class TestExtension extends ExtensionBuilder { + + private final ExtensionContext context; + private final ListenersSupport listeners; + private TestExecutionListenerLambdaAdapter listenerAdapter; + + /** + * Create extension. + * + * @param cfg config + * @param context junit context + * @param listeners listeners support + */ + public TestExtension(final ExtensionConfig cfg, + final ExtensionContext context, + final ListenersSupport listeners) { + super(cfg); + this.context = context; + this.listeners = listeners; + } + + /** + * Useful to bind debug options on the extension debug (no need for additional keys). + * + * @return true if debug is enabled on guicey extension + */ + public boolean isDebug() { + return cfg.tracker.debug; + } + + /** + * Shortcut to simplify detection on what phase extension was created: beforeAll or beforeEach. + * + * @return true if application started once per test class, false if application started for each test method + */ + public boolean isApplicationStartedForClass() { + return getJunitContext().getTestMethod().isEmpty(); + } + + /** + * This might be class or method context ("before all" or "before each"), depending on how guicey extension would + * be registered: in case of registration with a non-static field "before all" not called. + *

        + * Note that guicey provide static method for accessing objects, stored in context, like: + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport#lookupSupport( + * org.junit.jupiter.api.extension.ExtensionContext)} or + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport#lookupInjector( + * org.junit.jupiter.api.extension.ExtensionContext)}. + * + * @return test extension context + */ + public ExtensionContext getJunitContext() { + return context; + } + + /** + * Listen for test lifecycle. Useful when not only resource close is required (achievable by returning + * a closable object from setup), but writing a separate junit extension is not desirable. + * Moreover, this listener is synchronized with guicey extension lifecycle. + *

        + * Listener could also be registered with lambdas using on* methods like + * {@link #onApplicationStart(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener)}. + * Lambda version might be more convenient in case when setup object is a lambda itself. + * + * @param listener listener object + * @return builder instance for chained calls + */ + public TestExtension listen(final TestExecutionListener listener) { + listeners.addListener(listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#starting( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called before application start (could be beforeAll (default) or beforeEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStarting(final LambdaTestListener listener) { + registerListener(ListenerEvent.Starting, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#started( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called after application start (could be beforeAll (default) or beforeEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStart(final LambdaTestListener listener) { + registerListener(ListenerEvent.Started, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#beforeAll( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called (might not be called!) before all test methods + * @return builder instance for chained calls + */ + public TestExtension onBeforeAll(final LambdaTestListener listener) { + registerListener(ListenerEvent.BeforeAll, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#beforeEach( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called before each test method + * @return builder instance for chained calls + */ + public TestExtension onBeforeEach(final LambdaTestListener listener) { + registerListener(ListenerEvent.BeforeEach, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#afterEach( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called after each test method + * @return builder instance for chained calls + */ + public TestExtension onAfterEach(final LambdaTestListener listener) { + registerListener(ListenerEvent.AfterEach, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#afterAll( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called (might not be called!) after all test methods + * @return builder instance for chained calls + */ + public TestExtension onAfterAll(final LambdaTestListener listener) { + registerListener(ListenerEvent.AfterAll, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#stopping( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called before application stop (could be afterAll (default) or afterEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStopping(final LambdaTestListener listener) { + registerListener(ListenerEvent.Stopping, listener); + return this; + } + + /** + * Lambda version of {@link #listen(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)} + * for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener#stopped( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)}. Lambda listener version is more useful in + * case when setup object is declared as a lambda itself. + *

        + * Might be called multiple times. + * + * @param listener listener called after application stop (could be afterAll (default) or afterEach phase) + * @return builder instance for chained calls + */ + public TestExtension onApplicationStop(final LambdaTestListener listener) { + registerListener(ListenerEvent.Stopped, listener); + return this; + } + + /** + * Search for annotated fields with validation (a field type must be assignable to provided type). + * + * @param annotation annotation to search + * @param requiredFieldType required field type + * @param annotation type + * @param required filed minimal type + * @return annotated test fields (including fields from base test class). + */ + public List> findAnnotatedFields( + final Class annotation, + final Class requiredFieldType) { + return TestFieldUtils.findAnnotatedFields(context.getRequiredTestClass(), annotation, requiredFieldType); + } + + private void registerListener(final ListenerEvent event, final LambdaTestListener listener) { + // create adapter on demand and register once - all other lambdas will refer to the same adapter instance + if (listenerAdapter == null) { + listenerAdapter = new TestExecutionListenerLambdaAdapter(); + listen(listenerAdapter); + } + listenerAdapter.listen(event, listener); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedField.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedField.java new file mode 100644 index 000000000..712620de7 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedField.java @@ -0,0 +1,323 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.field; + +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.platform.commons.util.ReflectionUtils; +import ru.vyarus.java.generics.resolver.util.GenericsUtils; +import ru.vyarus.java.generics.resolver.util.map.EmptyGenericsMap; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Annotated field wrapper. Used to simplify work with test fields by hiding all required reflection. + * + * @param annotation type + * @param value type + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +@SuppressWarnings("PMD.GodClass") +public class AnnotatedField { + private final A annotation; + private final Field field; + private final Class testClass; + // custom data assigned during processing + private Map data; + // required to track value change (last value set or get) + private T cachedValue; + + /** + * Create annotated field for exact field and annotation. + * + * @param annotation field annotation + * @param field annotated field + * @param testClass test class where field was searcher + */ + public AnnotatedField(final A annotation, + final Field field, + final Class testClass) { + this.annotation = annotation; + this.field = ReflectionUtils.makeAccessible(field); + this.testClass = testClass; + } + + /** + * @return field annotation instance (type of annotation defined by initial fields search) + */ + public A getAnnotation() { + return annotation; + } + + /** + * @return field class type + */ + @SuppressWarnings("unchecked") + public Class getType() { + // no generics used - assume type variables will never be used for the filed type itself + return (Class) field.getType(); + } + + /** + * For example if the field type is: {@code RootType}. Then the method would return + * [Param1, Param2]. + *

        + * Implementation does not expect not resolved variables (simple case). + * + * @return type arguments + */ + public List> getTypeParameters() { + return GenericsUtils.resolveGenericsOf(field.getGenericType(), EmptyGenericsMap.getInstance()); + } + + /** + * @return class that declares field + */ + public Class getDeclaringClass() { + return field.getDeclaringClass(); + } + + /** + * @return field name + */ + public String getName() { + return field.getName(); + } + + /** + * @return field instance + */ + public Field getField() { + return field; + } + + /** + * @param instance test instance (or null for static field) + * @return test field value + * @throws java.lang.IllegalStateException if non-static field resolved with null instance or other error appear + */ + @SuppressWarnings("unchecked") + public T getValue(final Object instance) { + if (instance == null && !isStatic()) { + throw new IllegalStateException("Field " + toStringField() + + " is not static: test instance required for obtaining value"); + } + if (!isCompatible(instance)) { + throw new IllegalStateException("Invalid instance provided: " + + (instance == null ? null : instance.getClass()) + + " for field " + toStringField()); + } + try { + cachedValue = (T) field.get(instance); + return cachedValue; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to get field " + toStringField() + + " value", e); + } + } + + /** + * In case of nested test, there would be root class instance and nested instance. It is important to select + * the correct instance for field manipulation: the correct test instance would be selected by preserved test class. + * + * @param instances test instances + * @return field value + */ + public T getValue(final TestInstances instances) { + return getValue(findRequiredInstance(instances)); + } + + /** + * @param instance test instance (or null for static field) + * @param value field value + * @throws java.lang.IllegalStateException if non-static field set with null instance or other error appear + */ + public void setValue(final Object instance, final T value) { + if (instance == null && !isStatic()) { + throw new IllegalStateException("Field " + toStringField() + + " is not static: test instance required for setting value"); + } + if (!isCompatible(instance)) { + throw new IllegalStateException("Invalid instance provided: " + + (instance == null ? null : instance.getClass()) + + " for field " + toStringField()); + } + try { + field.set(instance, value); + cachedValue = value; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to set field " + toStringField() + " value to " + value, e); + } + } + + /** + * In case of nested test, there would be root class instance and nested instance. It is important to select + * the correct instance for field manipulation: the correct test instance would be selected by preserved test class. + * + * @param instances test instances + * @param value value to set + */ + public void setValue(final TestInstances instances, final T value) { + setValue(findRequiredInstance(instances), value); + } + + /** + * @return true if field is static + */ + public boolean isStatic() { + return Modifier.isStatic(field.getModifiers()); + } + + /** + * @return true if field declared directly in test class (not in base class) + */ + public boolean isTestOwnField() { + return field.getDeclaringClass().equals(testClass); + } + + /** + * Validation option: throw error if field is not static. + */ + public void requireStatic() { + if (!isStatic()) { + throw new IllegalStateException(String.format( + "Field %s annotated with @%s, must be static", + toStringField(), annotation.annotationType().getSimpleName() + )); + } + } + + /** + * Validation option: throw error if field is static. + */ + public void requireNonStatic() { + if (isStatic()) { + throw new IllegalStateException(String.format( + "Field %s annotated with @%s, must not be static", + toStringField(), annotation.annotationType().getSimpleName() + )); + } + } + + /** + * Use this method to avoid duplicate value readings when you're SURE that value was already resolved. + * + * @return last obtained or set value + */ + public Object getCachedValue() { + return cachedValue; + } + + /** + * Required to prevent incorrect usage (field resolution with a wrong instance). + * + * @param instance instance to check + * @return true if the provided instance is a field class instance, false otherwise + */ + public boolean isCompatible(final Object instance) { + return isStatic() || (instance != null && testClass.isAssignableFrom(instance.getClass())); + } + + /** + * In case of nested tests, test instances would contain multiple test instances. It is important to + * select the correct one (using preserved original test class). + * + * @param instances test instances + * @return test instance or null + * @throws java.lang.IllegalStateException if a test instances object provided but does not contain the + * required test instance + */ + public Object findRequiredInstance(final TestInstances instances) { + if (instances == null) { + return null; + } + return instances.findInstance(testClass).orElseThrow(() -> + new IllegalStateException("No test instance found for test class: " + testClass)); + } + + /** + * Apply custom value for the field object. Useful during field processing to mark is as processed or assign + * an additional state. + * + * @param key key + * @param value value + */ + public void setCustomData(final String key, final Object value) { + if (data == null) { + data = new HashMap<>(); + } + data.put(key, value); + } + + /** + * Get custom value. + * + * @param key key + * @param value type + * @return value or null + */ + @SuppressWarnings("unchecked") + public K getCustomData(final String key) { + return data == null ? null : (K) data.get(key); + } + + /** + * Note: if key set with null value - it would be considered as false. + * + * @param key key + * @return true if non null custom value set + */ + public boolean isCustomDataSet(final String key) { + return data != null && data.get(key) != null; + } + + /** + * Clear custom state. + */ + public void clearCustomData() { + if (data != null) { + data.clear(); + } + } + + /** + * @return to string field with class and reduced package + */ + public String toStringField() { + return TestFieldUtils.toString(field); + } + + /** + * Validates that field value not changed (it is the same value as was injected). + * + * @param instance test instance + * @return value + * @throws java.lang.IllegalStateException if field value differs from the injected value + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public T checkValueNotChanged(final TestInstances instance) { + final T currentValue = cachedValue; + // get will overwrite cache + final T value = getValue(instance); + if (value != currentValue) { + throw new IllegalStateException(String.format( + "Field %s annotated with @%s value was changed: most likely, it happen in test setup method, " + + "which is called after Injector startup and so too late to change binding values. " + + "Manual initialization is possible in field directly.", + toStringField(), annotation.annotationType().getSimpleName() + )); + } + return value; + } + + @Override + public String toString() { + return toStringField() + " (" + + "@" + annotation.annotationType().getSimpleName() + (isStatic() ? " static" : "") + + " " + field.getType().getSimpleName() + + ")"; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java new file mode 100644 index 000000000..2767eee74 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/AnnotatedTestFieldSetup.java @@ -0,0 +1,522 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.field; + +import com.google.inject.Binding; +import com.google.inject.spi.InstanceBinding; +import com.google.inject.spi.ProviderInstanceBinding; +import jakarta.inject.Provider; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for annotated test field extensions. The main purpose is to hide all complexity of junit test instance + * management (including potential nested tests support), simplifying actual extension implementation. + *

        + * Encapsulates: + *

        + *

        + * The overall process is: find all annotated fields, request supporting object initialization (field value might + * be provided by user), request a field value object to inject into test (not calling if already initialized). + *

        + * Each field is represented by a self-contained object + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField}), which could hold an additional managing + * object to avoid external state requirement (all related objects already presented in field instance - no need for + * an external state). + *

        + * It is recommended to encapsulate the main logic into a separate hook (a hook object would perform required + * application modifications). Setup object should only provide the required configuration to this hook. This + * should greatly simplify the extension and make it more testable. + *

        + * All methods are protected to simplify overriding logic if required. + *

        + * Implementation could be registered as usual {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} + * object (hook and listener would be registered automatically). + * + * @param annotation type + * @param required field type for auto validations (could be Object) + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +@SuppressWarnings({"PMD.TooManyMethods", "PMD.GodClass"}) +public abstract class AnnotatedTestFieldSetup implements + TestEnvironmentSetup, TestExecutionListener { + + /** + * Indicates user-provided value (field was pre-initialized by user). + */ + protected static final String FIELD_MANUAL = "manual_creation"; + /** + * Indicates injected field value (or pre-initialized value presence). + */ + protected static final String FIELD_INJECTED = "value_injected"; + + /** + * True if guicey extension start application per test class (for all methods). + */ + protected boolean appPerClass; + /** + * Test class. + */ + protected Class regTestClass; + /** + * Human-readable (if @DisplayName or spock used) class or method name. + */ + protected String setupContextName; + /** + * Resolved annotated fields. + */ + protected List> fields; + /** + * Junit context, used for fields search. Required for reporting (because the report would be generated + * on the different phase). + */ + protected ExtensionContext setupContext; + + // inner state + private final Class fieldAnnotation; + private final Class fieldType; + // test context storage key for resolved fields + private final String storageKey; + + // private to not confuse sub-classes with setup-only instances + private TestInstances regTestInstance; + + /** + * On extending, use a default constructor and specify required parameters manually. + * + * @param fieldAnnotation field annotation class + * @param fieldType required fields type (could be Object) + * @param storageKey key used to store a fields list in junit context (must be unique for extension) + */ + public AnnotatedTestFieldSetup(final Class fieldAnnotation, + final Class fieldType, + final String storageKey) { + this.fieldAnnotation = fieldAnnotation; + this.fieldType = fieldType; + this.storageKey = storageKey; + } + + @Override + public Object setup(final TestExtension extension) { + appPerClass = extension.isApplicationStartedForClass(); + setupContext = extension.getJunitContext(); + regTestClass = setupContext.getRequiredTestClass(); + regTestInstance = setupContext.getTestInstances().orElse(null); + this.setupContextName = TestSetupUtils.getContextTestName(setupContext); + + // find all annotated fields in test class (if not already found) + // For nested test and guice extension per method initialization, all fields resolved for top classes + // must also be included (otherwise their values would remain null) + fields = lookupFields(setupContext, () -> extension.findAnnotatedFields(fieldAnnotation, fieldType)); + if (!fields.isEmpty()) { + // avoid registration if no fields declared + registerHooks(extension); + extension.listen(this); + } + return null; + } + + /** + * Validate resolved field, if required. Note that some validations are performed automatically like + * checking field type with provided required type or unreachable annotated fields reporting. This method + * should be used for validations, which are not possible to perform automatically (e.g., there is a + * class, declared in annotation that must comply with a field type (base class know nothing about annotation and + * can't check that). + *

        + * Called only for current test class own fields: in case of nested test, root test fields would already be + * validated. Also, if guice context started per each test method, validation would be called only for the first + * test method because fields would be searched just once - no need to validate each time. + * + * @param context junit context + * @param field annotated fields + */ + protected abstract void fieldDetected(ExtensionContext context, AnnotatedField field); + + /** + * Called to register additional guicey hooks, if required. Called only when at least one annotated field is + * detected. + * + * @param extension extension configuration object + */ + protected abstract void registerHooks(TestExtension extension); + + + /** + * Configure application for a field (user value might be provided). There might be field object instance creation + * (e.g. mocks initialization), guice overrides registration, etc. The main initialization point. + *

        + * NOTE: If user-provided values are not allowed, throw an exception here + * + * @param field annotated field + * @param userValue user-provided field value (pre-initialized) + * @param type for aligning a binding key with value types (cheating on guice type checks) + */ + protected abstract void initializeField(AnnotatedField field, T userValue); + + /** + * Called after application startup and before field value injection. Useful for additional validations, which + * can't be performed before, like binding correctness validation (requiring the created injector): for example, + * to detect instance bindings when extension relies on AOP and so would not work. Such validation is impossible + * to do before (in time of binding overrides). + *

        + * Called before {@link #injectFieldValue(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext, + * AnnotatedField)}. At this point, non-static fields could be resolved (test instance present). + *

        + * Important: method called for all fields, even initialized by user! Inject value method might not be called + * after it! + * + * @param context event context + * @param field annotated field + */ + protected abstract void beforeValueInjection(EventContext context, AnnotatedField field); + + /** + * Get test field value (would be immediately injected into the test field). Called only if field was not + * initialized by user (not a manual value). For example, implementation might simply get bean instance + * from guice context (if guice was re-configured with module overrides). + *

        + * Warning: not called for manually initialized fields (because value already set)! To validate binding use + * {@link #beforeValueInjection(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext, AnnotatedField)} + * method instead (which is called for all fields). + *

        + * Application already completely started and test extension initialized at this moment (beforeEach test phase). + * + * @param context event context + * @param field annotated field + * @return created field value + */ + protected abstract T injectFieldValue(EventContext context, AnnotatedField field); + + /** + * Called when debug is enabled on guicey extension to report registered fields. + * Note: there might be fields from multiple test classes in case of nested tests. + *

        + * Report called after application startup because at this point all fields were processed (in configure guice + * method) and so all required fields data collected. Called only if at least one field is detected. + *

        + * Special custom data markers used in field objects + * ({@link AnnotatedField#getCustomData(String)}): + *

        + * + * @param context event context, IMPORTANT - this would be setup context and not current + * @param fields fields to report + */ + protected abstract void report(EventContext context, List> fields); + + /** + * Called before each test to pre-process field value (if required). + * + * @param context event context + * @param field filed descriptor + * @param value value instance + */ + protected abstract void beforeTest(EventContext context, AnnotatedField field, T value); + + /** + * Called after each test to post-process field value (if required). + * + * @param context event context + * @param field filed descriptor + * @param value value instance + */ + protected abstract void afterTest(EventContext context, AnnotatedField field, T value); + + // the application is ready to be started, but test instance is already available + @Override + public void starting(final EventContext context) throws Exception { + for (final AnnotatedField field : fields) { + // look if field already initialized + if (regTestInstance != null || field.isStatic()) { + final T existing = field.getValue(regTestInstance); + if (existing != null) { + // use manually initialized instance (field value) + initializeField(field, existing); + field.setCustomData(FIELD_MANUAL, true); + // mark static value as injected: no need to re-inject + field.setCustomData(FIELD_INJECTED, field.isStatic() ? field.getDeclaringClass() + : field.findRequiredInstance(regTestInstance)); + continue; + } + } + + // bind as type - guice will create instance + initializeField(field, null); + } + } + + // the application started + @Override + public void started(final EventContext context) { + // here because manual stubs detection will appear only during injector startup + if (context.isDebug() && !fields.isEmpty()) { + report(new EventContext(setupContext, true), fields); + } + } + + @Override + public void beforeAll(final EventContext context) { + // inject static fields + final Class testClass = context.getJunitContext().getRequiredTestClass(); + if (testClass == regTestClass) { + injectValues(context, fields, null); + } else { + // in case on nested tests - search for declared fields and fail because injector already created + validateUnreachableFieldsInNestedTest(testClass); + } + } + + @Override + public void beforeEach(final EventContext context) { + // inject non-static fields + final TestInstances testInstances = context.getJunitContext().getRequiredTestInstances(); + injectValues(context, fields, testInstances); + // call lifecycle methods on stub if required + valueLifecycle(context, fields, testInstances, true); + } + + @Override + public void afterEach(final EventContext context) { + // call lifecycle methods on stub if required + valueLifecycle(context, fields, context.getJunitContext().getRequiredTestInstances(), false); + } + + @Override + public void stopped(final EventContext context) { + // after app shutdown clear static fields injected with guice-managed bean + // otherwise it would be impossible to differentiate it from manual stub for the next test (on per method) + fields.forEach(field -> { + if (field.isStatic() && !field.isCustomDataSet(FIELD_MANUAL)) { + field.setValue(null, null); + field.clearCustomData(); + } + }); + } + + /** + * Resolve test own fields or use already resolved fields set (for example, when guicey extension created + * for each method we can search and validate fields just once). Also adds fields from all paren contexts + * (for nested tests, which must see parent test fields and so we need to manage its lifecycle) + * + * @param context junit context + * @param fieldsProvider fields searching logic + * @return all annotated fields + */ + protected List> lookupFields( + final ExtensionContext context, + final Provider>> fieldsProvider) { + final ExtensionContext ctx = getClassContext(context); + + // resolved fields are always stored under CLASS context + // so if extension created per method it would analyze fields just once + // Nested tests would also use already prepared parent fields (when extension created per method) + List> res = getOwnFields(ctx); + if (res == null) { + res = fieldsProvider.get(); + if (!res.isEmpty()) { + // validate only own fields - top level fields assumed to be already validated (we are inside nested + // test) + res.forEach(field -> fieldDetected(context, field)); + } + getStore(ctx).put(storageKey, res); + } + + // now looking for fields stored in parent contexts and adding all them (with state reset) + final List> inherited = getParentFields(ctx); + // reset parent state! + inherited.forEach(AnnotatedField::clearCustomData); + res.addAll(inherited); + return res; + } + + + + /** + * Inject field values into test instance (under beforeEach). User defined values stay as is. Value is injected + * only once for test instance. + * + * @param context event context + * @param fields annotated fields + * @param testInstances tests instances (might be several for nested tests) + */ + @SuppressWarnings({"CyclomaticComplexity", "PMD.SimplifiedTernary", "PMD.CompareObjectsWithEquals"}) + protected void injectValues(final EventContext context, + final List> fields, + final TestInstances testInstances) { + final boolean checkFieldValueInvisibleOnInitialization = testInstances != null && appPerClass; + fields.forEach(field -> { + if (checkFieldValueInvisibleOnInitialization && !field.isStatic()) { + // when injector created in beforeAll, it can't see instance fields, but later we can validate + // and fail on wrong usage + failIfInstanceFieldInitialized(field, testInstances); + } + beforeValueInjection(context, field); + + // exact instance required because it must be stored + final Object instance = field.findRequiredInstance(testInstances); + final boolean isAlreadyInjected = (field.isStatic() && field.isCustomDataSet(FIELD_INJECTED)) + // instance check required because field might be used in multiple test instances during + // injector lifetime + || (!field.isStatic() && instance == field.getCustomData(FIELD_INJECTED)); + // static fields might be not initialized in beforeAll (so do it in beforeEach) + if ((instance != null || field.isStatic()) && !isAlreadyInjected) { + field.setValue(instance, injectFieldValue(context, field)); + field.setCustomData(FIELD_INJECTED, field.isStatic() ? true : instance); + } + }); + } + + /** + * Called in beforeEach/afterEach to apply automatic lifecycle for field objects. + * + * @param context junit context + * @param fields annotated fields + * @param testInstances test instances (might be several for nested tests) + * @param before true for beforeEach, false for afterEach + */ + protected void valueLifecycle(final EventContext context, + final List> fields, + final TestInstances testInstances, + final boolean before) { + fields.forEach(field -> { + // value might be re-assigned in test setup method, but such change could be detected only after test: + // throwing error to, at least, indicate problem (otherwise would be a confusion point) + final T value = field.checkValueNotChanged(testInstances); + if (before) { + beforeTest(context, field, value); + } else { + afterTest(context, field, value); + } + }); + } + + /** + * @param field field + * @return universal prefix for field declaration errors + */ + protected String getDeclarationErrorPrefix(final AnnotatedField field) { + return "Incorrect @" + fieldAnnotation.getSimpleName() + " '" + field.toStringField() + "' declaration: "; + } + + /** + * When guicey extension starts in beforeAll - it can't see instance fields (by default) and so can't check + * if use provide any value. On beforeEach we have to validate that value was not provided, because it's too late - + * guice context was already created. + * + * @param field field to check + * @param testInstances test instances (might be several in case of nested tests) + */ + protected void failIfInstanceFieldInitialized(final AnnotatedField field, + final TestInstances testInstances) { + final Object value = field.getValue(testInstances); + if (value != null && !field.isCustomDataSet(FIELD_INJECTED)) { + throw new IllegalStateException(getDeclarationErrorPrefix(field) + "field value can't be used because " + + "guice context starts in beforeAll phase. Either make field static or remove value (" + + "guice will create instance with guice injector)"); + } + } + + /** + * When guicey extension created in beforeAll, same extension would be used for nested tests, which means + * that, if nested test declares any annotated fields, they can't be injected into already started guice + * context and so usage error must be reported. + * + * @param testClass nested test class + */ + protected void validateUnreachableFieldsInNestedTest(final Class testClass) { + final List> wrongFields = TestFieldUtils + .findAnnotatedFields(testClass, fieldAnnotation, fieldType); + if (!wrongFields.isEmpty()) { + throw new IllegalStateException(getDeclarationErrorPrefix(wrongFields.get(0)) + "nested test runs under " + + "already started application and so new fields could not be added. Either remove annotated" + + " fields in nested tests or run application for each test method (with non-static " + + "@RegisterExtension field)"); + } + } + + /** + * This is important for the nested tests - each nested test may have its own set of fields (if guicey extension + * created per test method). When guicey extension is created for nested test, we still need to resolve + * parent test fields to correctly initialize them and apply value lifecycle (because nested test could see parent + * fields). + * + * @param context junit context + * @return all fields in parent contexts + */ + protected List> getParentFields(final ExtensionContext context) { + final List> res = new ArrayList<>(); + ExtensionContext ctx = context.getParent().orElse(null); + while (ctx != null && ctx.getTestClass().isPresent()) { + final List> tmp = getOwnFields(ctx); + if (tmp != null) { + res.addAll(tmp); + } + ctx = ctx.getParent().orElse(null); + } + + return res; + } + + /** + * @param context junit context + * @return fields which belong to test class (ignoring fields in parent contexts) + */ + @SuppressWarnings("unchecked") + protected List> getOwnFields(final ExtensionContext context) { + return (List>) getStore(context).get(storageKey); + } + + /** + * Gets test-specific extension storage. + * + * @param context junit context (class level!) + * @return extension storage object + */ + protected ExtensionContext.Store getStore(final ExtensionContext context) { + // IMPORTANT getClass() used to store under different keys for different extensions + return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestClass())); + } + + /** + * Class context is the same for all test methods, and so it is suitable for storing something that must survive + * between test methods. + * + * @param context junit context + * @return class junit context + */ + protected ExtensionContext getClassContext(final ExtensionContext context) { + ExtensionContext ctx = context; + while (ctx.getTestMethod().isPresent()) { + ctx = ctx.getParent().get(); + } + return ctx; + } + + /** + * Note: covers only the simplest cases (just for a basic validations). + * + * @param binding binding + * @return true if guice does not manage bean instance, false otherwise + */ + protected boolean isInstanceBinding(final Binding binding) { + // Note: there might be other situations, like guice-managed provider, providing instances + // or longer binding chain. All cases are not checked intentionally - just the most obvious + return binding instanceof InstanceBinding || binding instanceof ProviderInstanceBinding; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/TestFieldUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/TestFieldUtils.java new file mode 100644 index 000000000..d1dd381b9 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/field/TestFieldUtils.java @@ -0,0 +1,111 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.field; + +import org.junit.platform.commons.support.AnnotationSupport; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Helper utility for search and processing annotated test fields (for test extensions implementation). + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public final class TestFieldUtils { + + private TestFieldUtils() { + } + + /** + * Search for annotated fields in test clas (including base test hierarchy). In the returned list static fields + * go first (sort applied). + * + * @param testClass test class + * @param ann field annotation + * @param requiredType required field type (if field is not a supertype of declared - error thrown) + * @param annotation type + * @param field (base) type + * @return detected fields + */ + public static List> findAnnotatedFields( + final Class testClass, + final Class ann, + final Class requiredType) { + final List fields = AnnotationSupport.findAnnotatedFields(testClass, ann); + + final List> res = new ArrayList<>(fields.size()); + for (Field field : fields) { + if (!requiredType.isAssignableFrom(field.getType())) { + throw new IllegalStateException(String.format( + "Field %s annotated with @%s, but its type is not %s", + toString(field), ann.getSimpleName(), requiredType.getSimpleName() + )); + } + res.add(new AnnotatedField<>(field.getAnnotation(ann), field, testClass)); + } + // sort static fields first + res.sort(Comparator.comparing(field -> field.isStatic() ? 0 : 1)); + return res; + } + + /** + * Filter fields, declared in base test classes (not own fields). + * + * @param fields fields to filter + * @param annotation type + * @param field type + * @return fields not directly declared in test class + */ + public static List> getInheritedFields( + final List> fields) { + return fields.stream() + .filter(fieldAccess -> !fieldAccess.isTestOwnField()) + .collect(Collectors.toList()); + } + + /** + * Filter fields, declared directly in test classes (own fields). + * + * @param fields fields to filter + * @param annotation type + * @param field type + * @return fields directly declared in test class + */ + public static List> getTestOwnFields( + final List> fields) { + return fields.stream() + .filter(AnnotatedField::isTestOwnField) + .collect(Collectors.toList()); + } + + /** + * Get multiple fields value at once. + * + * @param fields fields to get values from + * @param instance test instance (could by null for static fields) + * @param annotation type + * @param field type + * @return field values + */ + public static List getValues( + final List> fields, final Object instance) { + final List res = new ArrayList<>(); + for (final AnnotatedField field : fields) { + res.add(field.getValue(instance)); + } + return res; + } + + /** + * @param field field + * @return string field representation with class and reduced package (for reports and logs). + */ + public static String toString(final Field field) { + return RenderUtils.getFullClassName(field.getDeclaringClass()) + "." + field.getName(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java new file mode 100644 index 000000000..0c5aa0e4c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/EventContext.java @@ -0,0 +1,100 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen; + +import com.google.inject.Injector; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport; + +/** + * Event context wraps junit {@link org.junit.jupiter.api.extension.ExtensionContext} and provides access for + * the main test objects (like injector, test support and http client). Custom object is required mainly for + * lambda even listeners, which are unable to see default interface methods. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class EventContext { + + private final ExtensionContext context; + private final boolean debug; + + /** + * Create event context. + * + * @param context extension context + * @param debug true if debug enabled in extension + */ + public EventContext(final ExtensionContext context, final boolean debug) { + this.context = context; + this.debug = debug; + } + + /** + * @return junit context + */ + public ExtensionContext getJunitContext() { + return context; + } + + /** + * Useful to bind debug options on the extension debug (no need for additional keys). + * + * @return true if debug is enabled on guicey extension + */ + public boolean isDebug() { + return debug; + } + + /** + * Normally, it is impossible that support would not be found (under called lifecycle methods). + * + * @return dropwizard support object (or guicey support) + * @throws IllegalStateException if the support object not found (should be impossible) + */ + public DropwizardTestSupport getSupport() { + return GuiceyExtensionsSupport.lookupSupport(context) + .orElseThrow(() -> new IllegalStateException("Test support not found")); + } + + /** + * Normally, it is impossible that injector would not be found (under called lifecycle methods). + * + * @return injector instance + * @throws IllegalStateException if the injector object not found (should be impossible) + */ + public Injector getInjector() { + return GuiceyExtensionsSupport.lookupInjector(context) + .orElseThrow(() -> new IllegalStateException("Injector not found")); + } + + /** + * Shortcut to get bean directly from injector. + * + * @param type bean class + * @param bean type + * @return bean instance, never null (throw error if not found) + */ + public T getBean(final Class type) { + return getInjector().getInstance(type); + } + + /** + * Note that client is created even for pure guicey tests (in case if something external must be called). + * + * @return client instance + * @throws IllegalStateException if the client object not found (should be impossible) + */ + public ClientSupport getClient() { + return GuiceyExtensionsSupport.lookupClient(context) + .orElseThrow(() -> new IllegalStateException("Client not found")); + } + + /** + * @return true if complete application started, false for guice-only part + */ + public boolean isWebStarted() { + return !(getSupport() instanceof GuiceyTestSupport); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java new file mode 100644 index 000000000..e00ea934c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/TestExecutionListener.java @@ -0,0 +1,147 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen; + +/** + * Test listener allows listening for the main test events. There are no beforeAll and afterAll events directly + * because guicey extension could be created either on beforeAll or in beforeEach (depends on test configuration). + *

        + * Before/after test called before and after each test method. This could be used for custom setup/cleanup logic. + * BeforeAll and afterAll might not be called - use with caution (depends on extension registration). + *

        + * This interface provides a simple replacement for junit extension (synchronized with guicey lifecycle). + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public interface TestExecutionListener { + + /** + * Called before dropwizard (or guicey) application starting. It could be beforeAll or beforeEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

        + * NOTE: At this stage, injections not yet performed inside test instance. + *

        + * This method could be used instead of beforeAll because normally extension is created under beforeAll, but + * for extensions created under beforeEach - it would be impossible to notify about beforeAll anyway. + * + * @param context context object providing access to all available objects (junit context, test support, etc.) + * @throws java.lang.Exception on error + */ + default void starting(final EventContext context) throws Exception { + // default empty + } + + /** + * Called when dropwizard (or guicey) application started. It could be beforeAll or beforeEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

        + * NOTE: At this stage, injections not yet performed inside test instance. + *

        + * This method could be used instead of beforeAll because normally extension is created under beforeAll, but + * for extensions created under beforeEach - it would be impossible to notify about beforeAll anyway. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void started(final EventContext context) throws Exception { + // empty default + } + + /** + * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field + * (and so application created before each method). + * Prefer {@link #started(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)} instead, which is + * always called (but not always under beforeAll), + *

        + * Method could be useful if some action must be performed before each test (in case of nested tests or + * global application when "start" would not be called for each test). + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void beforeAll(final EventContext context) throws Exception { + // empty default + } + + /** + * Called before each test method execution. Guice injections into test instance already performed. + * Even if an application is created in beforeEach phase, this method would be called after application creation. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void beforeEach(final EventContext context) throws Exception { + // empty default + } + + /** + * Called after each test method execution. Even if an application is closed on afterEach, this method would be + * called before it. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void afterEach(final EventContext context) throws Exception { + // empty default + } + + /** + * IMPORTANT: this method MIGHT NOT BE CALLED at all in case if extension is registered under non-static field + * (and so the application is stopped after each method). + * Prefer {@link #stopped(ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext)} instead, which is + * always called (but not always under afterAll), + *

        + * Method could be useful if some action must be performed after each test (in case of nested tests or + * global application when "stop" would not be called for each test). + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void afterAll(final EventContext context) throws Exception { + // empty default + } + + /** + * Called before dropwizard (or guicey) application stopping. It could be afterAll or afterEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

        + * Note that in case of global application usage or for nested tests this method might not be called because + * application lifecycle would be managed by the top-most test. + *

        + * This method could be used instead of afterAll because normally extension is stopped under afterAll, but + * for extensions stopped under afterEach - it would be impossible to notify about afterAll anyway. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void stopping(final EventContext context) throws Exception { + // empty default + } + + /** + * Called when dropwizard (or guicey) application stopped. It could be afterAll or afterEach phase + * (if important, look {@link org.junit.jupiter.api.extension.ExtensionContext#getTestMethod()} to make sure). + * Application could start/stop multiple times within one test class (if extension registered in non-static field). + *

        + * Note that in case of global application usage or for nested tests this method might not be called because + * application lifecycle would be managed by the top-most test. + *

        + * This method could be used instead of afterAll because normally extension is stopped under afterAll, but + * for extensions stopped under afterEach - it would be impossible to notify about afterAll anyway. + * + * @param context context object providing access to all required objects (junit context, injector, + * test support, etc.) + * @throws java.lang.Exception on error + */ + default void stopped(final EventContext context) throws Exception { + // empty default + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java new file mode 100644 index 000000000..af4a396c6 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/LambdaTestListener.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; + +/** + * Lambda version for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}. Requires + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.TestExecutionListenerLambdaAdapter}. Assumed to + * be used with {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} object, when its declared + * as lambda itself and complete listener implementation ( + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension#listen( + * ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener)}) would look clumsy. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +@FunctionalInterface +public interface LambdaTestListener { + + /** + * Called on test event (see {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener}) + * for events description. + * + * @param context context object providing access to all required objects + * @throws java.lang.Exception on error + */ + void onTestEvent(EventContext context) throws Exception; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java new file mode 100644 index 000000000..e7c1794c2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/ListenerEvent.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +/** + * Test listener event definition for + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda.LambdaTestListener}. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public enum ListenerEvent { + /** + * Application starting. + */ + Starting, + /** + * Application started. + */ + Started, + /** + * Application stopping. + */ + Stopping, + /** + * Application stopped. + */ + Stopped, + /** + * Before all test methods. + */ + BeforeAll, + /** + * After all test methods. + */ + AfterAll, + /** + * Before each test method. + */ + BeforeEach, + /** + * After each test method. + */ + AfterEach +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java new file mode 100644 index 000000000..3db4e4d49 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/listen/lambda/TestExecutionListenerLambdaAdapter.java @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.env.listen.lambda; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; + +/** + * An adapter for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener} to be able to + * register each listener method with a lambada (more suitable for builder style, rather than direct interface + * implementation). + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class TestExecutionListenerLambdaAdapter implements TestExecutionListener { + + private final Multimap listeners = ArrayListMultimap.create(); + + /** + * Add lambda as an event listener. + * + * @param event target event + * @param listener listener to add + */ + public void listen(final ListenerEvent event, final LambdaTestListener listener) { + listeners.put(event, listener); + } + + @Override + public void starting(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.Starting); + } + + @Override + public void started(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.Started); + } + + @Override + public void beforeAll(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.BeforeAll); + } + + @Override + public void beforeEach(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.BeforeEach); + } + + @Override + public void afterEach(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.AfterEach); + } + + @Override + public void afterAll(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.AfterAll); + } + + @Override + public void stopping(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.Stopping); + } + + @Override + public void stopped(final EventContext context) throws Exception { + callListeners(context, ListenerEvent.Stopped); + } + + private void callListeners(final EventContext context, final ListenerEvent event) throws Exception { + for (LambdaTestListener listener : listeners.get(event)) { + listener.onTestEvent(context); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java new file mode 100644 index 000000000..79d2a2039 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java @@ -0,0 +1,634 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.inject.Injector; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.ListenersSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.TestFieldUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyFieldsSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackerFieldsSupport; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.HooksUtil; +import ru.vyarus.dropwizard.guice.test.util.ReusableAppUtils; +import ru.vyarus.dropwizard.guice.test.util.StoredReusableApp; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Base class for junit 5 extensions implementations. All extensions use {@link DropwizardTestSupport} object + * for actual execution (only configuration differs). + *

        + * Extensions might be used on class level (annotation and manual registration in static field; when extension start + * dropwizard app before all tests and shut down it after all tests) and on method level (manual registration in non + * static field; application starts before each test). + *

        + * Nested tests also supported. + *

        + * Test instance is not managed by guice! Only {@link com.google.inject.Injector#injectMembers(Object)} applied + * for it to process test fields injection. Guice AOP can't be used on test methods. Technically, creating test + * instances with guice is possible, but in this case nested tests could not work at all, which is unacceptable. + *

        + * Extension detects static fields of {@link GuiceyConfigurationHook} type, annotated with {@link EnableHook} + * and initialize these hooks automatically. It was done like this to simplify customizations, when main extension + * could be declared as annotation and hook as field. Also, it was impossible to implement hooks support + * with junit extension. Hook field could be declared even in base test class. + *

        + * Also, detects {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} fields annotated with + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup}. Behaviour is the same as with hook fields. + *

        + * For external integrations (other extensions), there is a special "hack" allowing to access + * {@link DropwizardTestSupport} object (and so get access to injector): {@link #lookupSupport(ExtensionContext)}. + * And shortcuts {@link #lookupInjector(ExtensionContext)} and {@link #lookupClient(ExtensionContext)}. + * + * @author Vyacheslav Rusakov + * @see TestParametersSupport for supported test parameters + * @since 29.04.2020 + */ +@SuppressWarnings({"PMD.ExcessiveImports", "ClassDataAbstractionCoupling", "ClassFanOutComplexity"}) +public abstract class GuiceyExtensionsSupport extends TestParametersSupport implements + BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback, + AfterEachCallback { + private static final Logger LOGGER = LoggerFactory.getLogger(GuiceyExtensionsSupport.class); + + // dropwizard support storage key (store visible for all relative tests) + private static final String DW_SUPPORT = "DW_SUPPORT"; + // injector storage key + private static final String INJECTOR = "INJECTOR"; + // storage key used to indicate reusable app usage instead of test-specific instance (even for first test) + private static final String DW_SUPPORT_GLOBAL = "DW_SUPPORT_GLOBAL"; + // ClientFactory instance + private static final String DW_CLIENT = "DW_CLIENT"; + // indicator storage key of nested test (when extension activated in parent test) + private static final String INHERITED_DW_SUPPORT = "INHERITED_DW_SUPPORT"; + // indicator storage key for case when application started for each method in test + private static final String PER_METHOD_DW_SUPPORT = "PER_METHOD_DW_SUPPORT"; + // list of test instance hashes where injection was performed (injectOnce option tracker) + private static final String INJECTION_INTSTANCE_HASH = "INJECTION_INTSTANCE_HASH"; + + // required for proper initialization under parallel tests + private static final Object SYNC = new Object(); + + /** + * Extensions registration tracker. + */ + protected final TestExtensionsTracker tracker; + /** + * Event listeners support. + */ + protected final ListenersSupport listeners; + + /** + * Create extensions support. + * + * @param tracker tracker + */ + public GuiceyExtensionsSupport(final TestExtensionsTracker tracker) { + this.tracker = tracker; + this.listeners = new ListenersSupport(tracker); + } + + @Override + public void beforeAll(final ExtensionContext context) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + tracker.lifecyclePhase(context, GuiceyTestTime.BeforeAll); + synchronized (SYNC) { + // check if app is reusable and start it (or apply already started to context) + checkReusableApp(context); + } + + if (!lookupSupport(context).isPresent()) { + start(context, null); + } else { + // in case of nested test, beforeAll for root extension will be called second time (because junit keeps + // only one extension instance!) and this means we should not perform initialization, but we also must + // prevent afterAll call for this nested test too and so need to store marker value! + + // Also, this branch works with reusable apps when only first test starts new application and other + // tests just use already started instance (just like with nested classes) + + final ExtensionContext.Store localStore = getLocalExtensionStore(context); + // just in case + Preconditions.checkState(localStore.get(INHERITED_DW_SUPPORT) == null, + "Storage assumptions were wrong or unexpected junit usage appear. " + + "Please report this case to guicey developer."); + localStore.put(INHERITED_DW_SUPPORT, true); + } + listeners.broadcastBeforeAll(context); + tracker.performanceTrack(GuiceyTestTime.BeforeAll, timer.elapsed()); + } + + @Override + public void beforeEach(final ExtensionContext context) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + tracker.lifecyclePhase(context, GuiceyTestTime.BeforeEach); + // run-per-method support (activated with @RegisterExtension on non-static field only) + if (!lookupSupport(context).isPresent()) { + start(context, context.getTestInstance().get()); + // mark per-method mode to properly shut down after test method + getLocalExtensionStore(context).put(PER_METHOD_DW_SUPPORT, true); + } + + // before each used to properly handle both default @TestInstance(TestInstance.Lifecycle.PER_METHOD) + // and @TestInstance(TestInstance.Lifecycle.PER_CLASS) (in later case BeforeAllCallback called after + // TestInstancePostProcessor, making it not usable for this task) + + injectMembers(context); + + listeners.broadcastBefore(context); + tracker.performanceTrack(GuiceyTestTime.BeforeEach, timer.elapsed()); + // log guicey time on each test method to see how overall time increase (and where) + tracker.logGuiceyTestTime(GuiceyTestTime.BeforeEach, context); + } + + @Override + public void afterEach(final ExtensionContext context) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + tracker.lifecyclePhase(context, GuiceyTestTime.AfterEach); + final boolean perMethod = getLocalExtensionStore(context).get(PER_METHOD_DW_SUPPORT) != null; + if (perMethod) { + stop(context); + } + listeners.broadcastAfter(context); + tracker.performanceTrack(GuiceyTestTime.AfterEach, timer.elapsed()); + if (perMethod) { + tracker.logGuiceyTestTime(GuiceyTestTime.AfterEach, context); + } + } + + @Override + public void afterAll(final ExtensionContext context) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + tracker.lifecyclePhase(context, GuiceyTestTime.AfterAll); + // do nothing in application per test method mode + if (lookupSupport(context).isPresent() + // nested tests support: do nothing for nested - extension managed on upper context + && getLocalExtensionStore(context).remove(INHERITED_DW_SUPPORT) == null) { + stop(context); + } + listeners.broadcastAfterAll(context); + tracker.performanceTrack(GuiceyTestTime.AfterAll, timer.elapsed()); + tracker.logGuiceyTestTime(GuiceyTestTime.AfterAll, context); + } + + // --------------------------------------------------------- 3rd party extensions support + + /** + * Static "hack" for other extensions extending base guicey extensions abilities. + *

        + * The only thin moment here is extensions order! Junit preserve declaration order so in most cases it + * should not be a problem. + * + * @param extensionContext extension context + * @param configuration type + * @return dropwizard support object prepared by guicey extension, or empty optional if no guicey extension used or + * its beforeAll hook was not called yet + */ + @SuppressWarnings("unchecked") + public static Optional> lookupSupport( + final ExtensionContext extensionContext) { + return Optional.ofNullable((DropwizardTestSupport) getExtensionStore(extensionContext).get(DW_SUPPORT)); + } + + /** + * Lookup test-specific injector. + *

        + * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup} mechanism not used here because + * it does not provide injector after application stop, which is not very usable for tests. + * + * @param extensionContext extension context + * @return application injector or empty optional + */ + public static Optional lookupInjector(final ExtensionContext extensionContext) { + return Optional.ofNullable((Injector) getExtensionStore(extensionContext).get(INJECTOR)); + } + + /** + * Shortcut for {@link ClientSupport} object lookup by other extensions. + *

        + * Custom extension must be activated after main guicey extension! + * + * @param extensionContext extension context + * @return client factory object or empty optional + */ + public static Optional lookupClient(final ExtensionContext extensionContext) { + return Optional.ofNullable((ClientSupport) getExtensionStore(extensionContext).get(DW_CLIENT)); + } + + /** + * Shortcut for testing if current test is using reusable application instead of test-specific application + * instance. + * + * @param extensionContext extension context + * @return true if global application instance used, false otherwise + */ + public static boolean isReusableAppUsed(final ExtensionContext extensionContext) { + return getExtensionStore(extensionContext).get(DW_SUPPORT_GLOBAL) != null; + } + + /** + * Close global application instance. Do nothing if no global application registered + * for provided base class. + *

        + * Method exists to allow creation of custom extension like "@CloseAppAfterTest" to + * be able to close app at some points (next test would start a fresh app again). + *

        + * In order to close application before test use {@link ReusableAppUtils} directly (it must be called + * before extension (which should start new app instance) and so there is no base class in context yet. + * + * @param extensionContext extension context + * @return true if app was closed, false otherwise + */ + public static boolean closeReusableApp(final ExtensionContext extensionContext) { + final Class baseClass = (Class) getExtensionStore(extensionContext).get(DW_SUPPORT_GLOBAL); + return baseClass != null && ReusableAppUtils.closeGlobalApp(extensionContext, baseClass); + } + + // --------------------------------------------------------- end of 3rd party extensions support + + /** + * Returns existing config or parse it from annotation. + *

        + * Separate configuration creation is important for application re-use logic when additional actions + * from {@link #prepareTestSupport(String, org.junit.jupiter.api.extension.ExtensionContext, java.util.List)} + * should be omitted in case if context already created. + * + * @param context extension context + * @return extension configuration + */ + protected abstract ExtensionConfig getConfig(ExtensionContext context); + + /** + * The only role of actual extension class is to configure {@link DropwizardTestSupport} object + * according to annotation. + * + * @param configPrefix configuration properties prefix + * @param context extension context + * @param setups setup extensions resolved from fields (or empty list) + * @return configured dropwizard test support object + */ + protected abstract DropwizardTestSupport prepareTestSupport(String configPrefix, + ExtensionContext context, + List setups); + + @Override + protected DropwizardTestSupport getSupport(final ExtensionContext extensionContext) { + return lookupSupport(extensionContext).orElse(null); + } + + @Override + protected ClientSupport getClient(final ExtensionContext extensionContext) { + return lookupClient(extensionContext).orElse(null); + } + + @Override + protected Optional getInjector(final ExtensionContext extensionContext) { + return lookupInjector(extensionContext); + } + + /** + * @param context junit context + * @return extension-specific store + */ + protected static ExtensionContext.Store getExtensionStore(final ExtensionContext context) { + // Store is extension specific, but nested tests will see it too (because key is extension class) + return context.getStore(ExtensionContext.Namespace.create(GuiceyExtensionsSupport.class)); + } + + /** + * @param context junit context + * @return extension-specific store for current test + */ + private ExtensionContext.Store getLocalExtensionStore(final ExtensionContext context) { + // test scoped extension scope (required to differentiate nested classes or parameterized executions) + return context.getStore(ExtensionContext.Namespace + .create(GuiceyExtensionsSupport.class, context.getRequiredTestClass())); + } + + private void checkReusableApp(final ExtensionContext context) throws Exception { + final ExtensionConfig config = getConfig(context); + if (config.reuseApp) { + final String source = config.reuseSource; + final StoredReusableApp globalApp = ReusableAppUtils.getGlobalApp(context, config.reuseDeclarationClass); + final ExtensionContext.Store store = getExtensionStore(context); + if (globalApp != null) { + Preconditions.checkState(globalApp.getSource().equals(config.reuseSource), + "Can't apply reusable app instance from %s in test %s because different reusable app (%s)" + + "is already registered", source, context.getRequiredTestClass().getSimpleName(), + globalApp.getSource()); + // highlight ignored extensions (@EnableSetup, @EnableSetup) + final Stopwatch timer = Stopwatch.createStarted(); + new FieldSupport(context.getRequiredTestClass(), null, null) + .hintIgnoredFields(config.reuseDeclarationClass); + tracker.performanceTrack(GuiceyTestTime.ReusableAppWarnings, timer.elapsed()); + if (store.get(DW_SUPPORT) == null) { + // global app already started, simply use it for current test (extension treat this case the same + // way as with nested test) + store.put(DW_SUPPORT, globalApp.getSupport()); + store.put(INJECTOR, TestSupport.getInjector(globalApp.getSupport())); + // new client created for each test + store.put(DW_CLIENT, globalApp.getClient()); + } else { + // DW_SUPPORT might be available at this point in nested tests + Preconditions.checkState(store.get(DW_SUPPORT).equals(globalApp.getSupport()), + "Can't apply reusable app instance from %s in test %s context because it already contains" + + " started app", source, context.getRequiredTestClass().getSimpleName()); + } + } else { + // start new application and apply it into global store + start(context, null); + // global context would close after all tests and would automatically call app close + ReusableAppUtils.registerGlobalApp(context, new StoredReusableApp( + config.reuseDeclarationClass, source, lookupSupport(context).get(), + lookupClient(context).get())); + } + // exists in nested tests + if (store.get(DW_SUPPORT_GLOBAL) == null) { + // simply to indicate reusable app usage + store.put(DW_SUPPORT_GLOBAL, config.reuseDeclarationClass); + } + } + } + + private void start(final ExtensionContext context, final Object testInstance) throws Exception { + // trigger config resolution from annotation and validate reusable app usage correctness for per-method + final ExtensionConfig config = getConfig(context); + final ExtensionContext.Store store = getExtensionStore(context); + // find fields annotated with @EnableHook and @EnableSetup + final FieldSupport fields = new FieldSupport(context.getRequiredTestClass(), testInstance, tracker); + if (config.reuseApp) { + fields.hintIncorrectFieldsUsage(config.reuseDeclarationClass); + } + fields.activateBaseHooks(); + + // config overrides work through system properties, so it is important to have unique prefixes + final String configPrefix = ConfigOverrideUtils.createPrefix(context); + final DropwizardTestSupport support = prepareTestSupport(configPrefix, context, + addDefaultSetupObjects(fields.getSetupObjects(), config.defaultExtensionsEnabled)); + // activate hooks declared in test static fields (so hooks declared in annotation goes before) + fields.activateClassHooks(); + store.put(DW_SUPPORT, support); + // for pure guicey tests client may seem redundant, but it can be used for calling other services + final ClientSupport client = new ClientSupport(support, config.clientFactory); + store.put(DW_CLIENT, client); + // to be able to access the support object outside of extension context + TestSupportHolder.setContext(support, client); + + tracker.enableDebugFromSystemProperty(); + listeners.broadcastStarting(context); + tracker.logUsedHooksAndSetupObjects(configPrefix); + final Stopwatch timer = Stopwatch.createStarted(); + support.before(); + // store injector directly because InjectorLookup mechanism would not work after application stop + store.put(INJECTOR, TestSupport.getInjector(support)); + tracker.performanceTrack(GuiceyTestTime.SupportStart, timer.elapsed()); + tracker.logOverriddenConfigs(configPrefix); + listeners.broadcastStart(context); + } + + private void stop(final ExtensionContext context) throws Exception { + // just in case, normally hooks cleared automatically after appliance + ConfigurationHooksSupport.reset(); + + final DropwizardTestSupport support = getSupport(context); + if (support != null) { + listeners.broadcastStopping(context); + final Stopwatch timer = Stopwatch.createStarted(); + support.after(); + TestSupportHolder.reset(); + tracker.performanceTrack(GuiceyTestTime.SupportStop, timer.stop().elapsed()); + listeners.broadcastStop(context); + // just in case (might not be called automatically for guicey test without managed lifecycle) + if (lookupInjector(context).isPresent()) { + // if startup failed, there would be no environment object - to avoid confusing error verifying by + // injector + SharedConfigurationState.get(support.getEnvironment()).ifPresent(SharedConfigurationState::shutdown); + } + } + final ClientSupport client = getClient(context); + if (client != null) { + client.close(); + } + } + + private List addDefaultSetupObjects(final List fields, + final boolean useDefaultExtensions) { + // fields resolution already tracked, so they must be registered first + final List res = new ArrayList<>(fields); + if (useDefaultExtensions) { + res.addAll(tracker.lookupExtensions( + TestSetupUtils.lookup() + )); + // extensions use aop and so must go last + res.addAll(tracker.defaultExtensions( + new SpyFieldsSupport(), + new TrackerFieldsSupport() + )); + + } + return res; + } + + @SuppressWarnings("unchecked") + private void injectMembers(final ExtensionContext context) { + final Object testInstance = context.getTestInstance() + .orElseThrow(() -> new IllegalStateException("Unable to get the current test instance")); + final Integer instanceHash = System.identityHashCode(testInstance); + + final DropwizardTestSupport support = Preconditions.checkNotNull(getSupport(context), + "Guicey test support was not initialized: most likely, you are trying to manually " + + "register extension using non-static field - such usage is not supported."); + + // parent would always present as current is a method context + final ExtensionContext.Store localStore = getLocalExtensionStore(context.getParent().get()); + Set lastInstanceHash = (Set) localStore.get(INJECTION_INTSTANCE_HASH, Set.class); + if (lastInstanceHash == null) { + lastInstanceHash = new HashSet<>(); + } + + final boolean injectOnce = getConfig(context).injectOnce; + // inject each time, except if only one injection requested per test instance + if (!injectOnce || !lastInstanceHash.contains(instanceHash)) { + tracker.performanceTrack(GuiceyTestTime.GuiceInjection, + TestSupport.injectBeans(support, testInstance)); + lastInstanceHash.add(instanceHash); + localStore.put(INJECTION_INTSTANCE_HASH, lastInstanceHash); + } + } + + /** + * Utility class for activating hooks and setup objects collected from fields (annotated with + * {@link EnableHook} and {link {@link ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup}}). + *

        + * Hook fields must be activated in two steps: first hooks declared in base classes, then hooks declared directly + * in test class (after hooks declared in extension would be activated). + *

        + * Setup extensions from fields are always registered after all other. + */ + private static class FieldSupport { + private final Class testClass; + // test instance in application per test method case (beforeEach) + private final Object instance; + private final TestExtensionsTracker tracker; + private final List> parentHookFields; + private final List> ownHookFields; + private final List> extensionFields; + + FieldSupport(final Class testClass, final Object instance, final TestExtensionsTracker tracker) { + this.testClass = testClass; + this.instance = instance; + this.tracker = tracker; + + final Stopwatch timer = Stopwatch.createStarted(); + final boolean staticFieldsRequired = instance == null; + // find and validate all fields + ownHookFields = TestFieldUtils.findAnnotatedFields( + testClass, EnableHook.class, GuiceyConfigurationHook.class); + if (staticFieldsRequired) { + // only static fields could be processed for beforeAll + ownHookFields.forEach(AnnotatedField::requireStatic); + } + parentHookFields = TestFieldUtils.getInheritedFields(ownHookFields); + ownHookFields.removeAll(parentHookFields); + if (tracker != null) { + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + + timer.reset().start(); + extensionFields = TestFieldUtils.findAnnotatedFields( + testClass, EnableSetup.class, TestEnvironmentSetup.class); + if (staticFieldsRequired) { + // only static fields could be processed for beforeAll + extensionFields.forEach(AnnotatedField::requireStatic); + } + if (tracker != null) { + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + } + + /** + * Extensions like {@link ru.vyarus.dropwizard.guice.test.EnableHook} and + * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup} might be declared in test class + * or on one of base classes, extending class with reusable app declaration. Such extensions would be + * used for reusable app startup, when this exact class is called first, but would be ignored when + * application would be started from different class. + *

        + * This usage is allowed for edge cases (for some debug), but, generally, this is incorrect usage. + * + * @param declarationClass base class where reusable app is declared + */ + public void hintIncorrectFieldsUsage(final Class declarationClass) { + final Stopwatch timer = Stopwatch.createStarted(); + final List wrong = findNonBaseFields(declarationClass); + if (!wrong.isEmpty()) { + LOGGER.warn("The following extensions were used during reusable app startup in test {}, but they did " + + "not belong to base class {} hierarchy where reusable app is declared and so would " + + "be ignored if reusable app would start by different test: \n{}", + testClass.getName(), declarationClass.getName(), Joiner.on("\n").join(wrong)); + } + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + + /** + * If application was already started by different tests then all extension fields not declared not in + * base class would be ignored (application already started). + * + * @param declarationClass base class where reusable app is declared + */ + public void hintIgnoredFields(final Class declarationClass) { + final Stopwatch timer = Stopwatch.createStarted(); + final List wrong = findNonBaseFields(declarationClass); + if (!wrong.isEmpty()) { + LOGGER.warn("The following extensions were ignored in test {} because reusable application was " + + "already started by another test: \n{}", + testClass.getName(), Joiner.on("\n").join(wrong)); + } + if (tracker != null) { + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + } + + public List getSetupObjects() { + final Stopwatch timer = Stopwatch.createStarted(); + try { + tracker.extensionsFromFields(extensionFields, instance); + return extensionFields.isEmpty() ? Collections.emptyList() + : TestFieldUtils.getValues(extensionFields, instance); + } finally { + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + } + + public void activateBaseHooks() { + final Stopwatch timer = Stopwatch.createStarted(); + // activate hooks declared in base classes + activateFieldHooks(parentHookFields); + tracker.hooksFromFields(parentHookFields, true, instance); + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + + public void activateClassHooks() { + final Stopwatch timer = Stopwatch.createStarted(); + // activate all remaining hooks (in test class) + activateFieldHooks(ownHookFields); + tracker.hooksFromFields(ownHookFields, false, instance); + tracker.performanceTrack(GuiceyTestTime.GuiceyFieldsSearch, timer.stop().elapsed()); + } + + @SuppressWarnings("unchecked") + private List findNonBaseFields(final Class declarationClass) { + final List wrong = new ArrayList<>(); + checkFieldsDeclaration(wrong, (List>) (List) parentHookFields, declarationClass); + checkFieldsDeclaration(wrong, (List>) (List) ownHookFields, declarationClass); + checkFieldsDeclaration(wrong, (List>) (List) extensionFields, declarationClass); + return wrong; + } + + private void checkFieldsDeclaration(final List wrong, + final List> fields, + final Class baseClass) { + fields.forEach(field -> { + if (!field.getDeclaringClass().isAssignableFrom(baseClass)) { + wrong.add("\t" + field.getDeclaringClass().getName() + "." + field.getName() + + " (" + field.getType().getSimpleName() + ")"); + } + }); + } + + private void activateFieldHooks(final List> fields) { + final Stopwatch timer = Stopwatch.createStarted(); + HooksUtil.register(TestFieldUtils.getValues(fields, instance)); + tracker.performanceTrack(GuiceyTestTime.HooksRegistration, timer.elapsed()); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java similarity index 72% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java index aa567ffa7..3dfe18c32 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestDropwizardAppExtension.java @@ -1,28 +1,38 @@ package ru.vyarus.dropwizard.guice.test.jupiter.ext; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; -import io.dropwizard.Application; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.server.AbstractServerFactory; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.platform.commons.support.AnnotationSupport; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory; import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; import ru.vyarus.dropwizard.guice.test.util.ConfigurablePrefix; import ru.vyarus.dropwizard.guice.test.util.HooksUtil; import ru.vyarus.dropwizard.guice.test.util.RandomPortsListener; +import ru.vyarus.dropwizard.guice.test.util.ReusableAppUtils; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; +import java.lang.reflect.AnnotatedElement; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.function.Function; /** * {@link TestDropwizardApp} junit 5 extension implementation. Normally, extension should be activated with annotation, @@ -58,12 +68,14 @@ * @author Vyacheslav Rusakov * @since 28.04.2020 */ +@SuppressWarnings("PMD.ExcessiveImports") public class TestDropwizardAppExtension extends GuiceyExtensionsSupport { - private static final String STAR = "*"; - private Config config; + /** + * Create extension. + */ public TestDropwizardAppExtension() { // extension created automatically by @TestGuiceyApp annotation super(new TestExtensionsTracker()); @@ -90,25 +102,24 @@ private TestDropwizardAppExtension(final Config config) { * additional dropwizard or guicey bundle (which will be the same as listener above). * * @param app application class + * @param configuration type * @return builder for extension configuration. */ - public static Builder forApp(final Class app) { - return new Builder(app); + public static Builder forApp(final Class> app) { + return new Builder<>(app); } @Override - @SuppressWarnings("unchecked") - protected DropwizardTestSupport prepareTestSupport(final String configPrefix, - final ExtensionContext context, - final List setups) { + protected ExtensionConfig getConfig(final ExtensionContext context) { if (config == null) { // Configure from annotation // Note that it is impossible to have both manually build config and annotation because annotation // will be processed first and manual registration will be simply ignored + final Optional element = context.getElement(); final TestDropwizardApp ann = AnnotationSupport // also search annotation inside other annotations (meta) - .findAnnotation(context.getElement(), TestDropwizardApp.class).orElse(null); + .findAnnotation(element, TestDropwizardApp.class).orElse(null); // catch incorrect usage by direct @ExtendWith(...) Preconditions.checkNotNull(ann, "%s annotation not declared: can't work without configuration, " @@ -117,34 +128,66 @@ protected DropwizardTestSupport prepareTestSupport(final String configPrefix, RegisterExtension.class.getSimpleName()); config = Config.parse(ann, tracker); + if (config.reuseApp) { + // identify annotation extension source (class) and validate declaration correctness + ReusableAppUtils.registerAnnotation(context.getRequiredTestClass(), ann, config); + } + } + if (config.reuseApp && config.reuseSource == null) { + // identify manual extension source (field) and validate declaration correctness + ReusableAppUtils.registerField(context.getRequiredTestClass(), this, config); } + return config; + } + @Override + @SuppressWarnings({"unchecked", "PMD.UseDiamondOperator"}) + protected DropwizardTestSupport prepareTestSupport(final String configPrefix, + final ExtensionContext context, + final List setups) { // setups from @EnableSetup fields go last config.extensions.addAll(setups); - TestSetupUtils.executeSetup(config, context); + TestSetupUtils.executeSetup(config, context, listeners); + final Stopwatch timer = Stopwatch.createStarted(); HooksUtil.register(config.hooks); - - final DropwizardTestSupport support = new DropwizardTestSupport(config.app, + tracker.performanceTrack(GuiceyTestTime.HooksRegistration, timer.elapsed()); + + timer.reset().start(); + final Configuration manualConfig = config.getConfiguration(config.configPath); + if (manualConfig != null && config.restMapping != null && !config.restMapping.isEmpty()) { + // rest mapping can't be applied with config override in case of a raw config object, + // so need to use modifier instead + config.configModifiers.add(conf -> ((AbstractServerFactory) conf.getServerFactory()) + .setJerseyRootPath(ConfigOverrideUtils.formatRestMapping(config.restMapping))); + } + final DropwizardTestSupport support = manualConfig == null + ? new DropwizardTestSupport(config.app, config.configPath, + null, configPrefix, - buildConfigOverrides(configPrefix, context)); + createCommandFactory(), + buildConfigOverrides(configPrefix, context)) + : new DropwizardTestSupport(config.app, manualConfig, createCommandFactory()); + tracker.performanceTrack(GuiceyTestTime.DropwizardTestSupport, timer.elapsed()); if (config.randomPorts) { - support.addListener(new RandomPortsListener()); + support.addListener(new RandomPortsListener<>()); } return support; } + @SuppressWarnings("unchecked") + private Function, Command> createCommandFactory() { + return ConfigOverrideUtils.buildCommandFactory((List>) (List) config.configModifiers); + } + @SuppressWarnings("unchecked") private ConfigOverride[] buildConfigOverrides( final String prefix, final ExtensionContext context) { ConfigOverride[] overrides = ConfigOverrideUtils.convert(prefix, config.configOverrides); if (!Strings.isNullOrEmpty(config.restMapping)) { - String mapping = PathUtils.leadingSlash(config.restMapping); - if (!mapping.endsWith(STAR)) { - mapping = PathUtils.trailingSlash(mapping) + STAR; - } - overrides = ConfigOverrideUtils.merge(overrides, ConfigOverride.config(prefix, "server.rootPath", mapping)); + overrides = ConfigOverrideUtils.merge(overrides, + ConfigOverrideUtils.overrideRestMapping(prefix, config.restMapping)); } return config.configOverrideObjects.isEmpty() ? overrides : ConfigOverrideUtils.merge(overrides, @@ -157,10 +200,17 @@ private ConfigOverride[] buildCo /** * Builder used for manual extension registration ({@link #forApp(Class)}). + * + * @param configuration type (resolved automatically by application class) */ - public static class Builder extends ExtensionBuilder { + public static class Builder extends ExtensionBuilder, Config> { - public Builder(final Class app) { + /** + * Create builder. + * + * @param app application class + */ + public Builder(final Class> app) { super(new Config()); this.cfg.app = Preconditions.checkNotNull(app, "Application class must be provided"); } @@ -171,7 +221,7 @@ public Builder(final Class app) { * @param configPath configuration file path * @return builder instance for chained calls */ - public Builder config(final String configPath) { + public Builder config(final String configPath) { cfg.configPath = configPath; return this; } @@ -182,7 +232,7 @@ public Builder config(final String configPath) { * @param randomPorts true to use random ports * @return builder instance for chained calls */ - public Builder randomPorts(final boolean randomPorts) { + public Builder randomPorts(final boolean randomPorts) { cfg.randomPorts = randomPorts; return this; } @@ -192,7 +242,7 @@ public Builder randomPorts(final boolean randomPorts) { * * @return builder instance for chained calls */ - public Builder randomPorts() { + public Builder randomPorts() { return randomPorts(true); } @@ -202,7 +252,7 @@ public Builder randomPorts() { * @param mapping rest mapping path * @return builder instance for chained calls */ - public Builder restMapping(final String mapping) { + public Builder restMapping(final String mapping) { cfg.restMapping = mapping; return this; } @@ -226,7 +276,7 @@ public Builder restMapping(final String mapping) { * @return builder instance for chained calls */ @SafeVarargs - public final Builder setup(final Class... support) { + public final Builder setup(final Class... support) { cfg.extensionsClasses(support); return this; } @@ -248,7 +298,7 @@ public final Builder setup(final Class... suppor * @param support support object instances * @return builder instance for chained calls */ - public Builder setup(final TestEnvironmentSetup... support) { + public Builder setup(final TestEnvironmentSetup... support) { cfg.extensionInstances(support); return this; } @@ -269,7 +319,7 @@ public TestDropwizardAppExtension create() { /** * Unified configuration. */ - @SuppressWarnings({"checkstyle:VisibilityModifier", "PMD.DefaultPackage"}) + @SuppressWarnings("checkstyle:VisibilityModifier") private static class Config extends ExtensionConfig { Class app; String configPath = ""; @@ -308,9 +358,18 @@ static Config parse(final TestDropwizardApp ann, final TestExtensionsTracker tra res.configOverrides = ann.configOverride(); res.randomPorts = ann.randomPorts(); res.restMapping = ann.restMapping(); + res.configModifiersFromAnnotation(ann.annotationType(), ann.configModifiers()); res.hooksFromAnnotation(ann.annotationType(), ann.hooks()); res.extensionsFromAnnotation(ann.annotationType(), ann.setup()); + res.injectOnce = ann.injectOnce(); res.tracker.debug = ann.debug(); + res.reuseApp = ann.reuseApplication(); + res.defaultExtensionsEnabled = ann.useDefaultExtensions(); + if (ann.apacheClient()) { + res.clientFactory(ApacheTestClientFactory.class); + } else { + res.clientFactory(ann.clientFactory()); + } return res; } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java similarity index 72% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java index bef28b53e..99cb656a8 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestGuiceyAppExtension.java @@ -1,8 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.ext; import com.google.common.base.Preconditions; -import io.dropwizard.Application; -import io.dropwizard.Configuration; +import com.google.common.base.Stopwatch; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.extension.ExtensionContext; @@ -10,18 +11,24 @@ import org.junit.platform.commons.support.AnnotationSupport; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; import ru.vyarus.dropwizard.guice.test.util.ConfigurablePrefix; import ru.vyarus.dropwizard.guice.test.util.HooksUtil; +import ru.vyarus.dropwizard.guice.test.util.ReusableAppUtils; import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; +import java.lang.reflect.AnnotatedElement; import java.util.Collections; import java.util.List; +import java.util.Optional; /** * {@link TestGuiceyApp} junit 5 extension implementation. Normally, extension should be activated with annotation, @@ -61,6 +68,9 @@ public class TestGuiceyAppExtension extends GuiceyExtensionsSupport { private Config config; + /** + * Create extension. + */ public TestGuiceyAppExtension() { // extension created automatically by @TestGuiceyApp annotation super(new TestExtensionsTracker()); @@ -87,24 +97,24 @@ private TestGuiceyAppExtension(final Config config) { * additional dropwizard bundle (which will be the same as listener above). * * @param app application class + * @param configuration type * @return builder for extension configuration. */ - public static Builder forApp(final Class app) { - return new Builder(app); + public static Builder forApp(final Class> app) { + return new Builder<>(app); } @Override - protected DropwizardTestSupport prepareTestSupport(final String configPrefix, - final ExtensionContext context, - final List setups) { + protected ExtensionConfig getConfig(final ExtensionContext context) { if (config == null) { // Configure from annotation // Note that it is impossible to have both manually build config and annotation because annotation // will be processed first and manual registration will be simply ignored + final Optional element = context.getElement(); final TestGuiceyApp ann = AnnotationSupport // also search annotation inside other annotations (meta) - .findAnnotation(context.getElement(), TestGuiceyApp.class).orElse(null); + .findAnnotation(element, TestGuiceyApp.class).orElse(null); // catch incorrect usage by direct @ExtendWith(...) Preconditions.checkNotNull(ann, "%s annotation not declared: can't work without configuration, " @@ -112,14 +122,33 @@ protected DropwizardTestSupport prepareTestSupport(final String configPrefix, TestGuiceyApp.class.getSimpleName(), RegisterExtension.class.getSimpleName()); config = Config.parse(ann, tracker); + if (config.reuseApp) { + // identify annotation extension source (class) and validate declaration correctness + ReusableAppUtils.registerAnnotation(context.getRequiredTestClass(), ann, config); + } + } + if (config.reuseApp && config.reuseSource == null) { + // identify manual extension source (field) and validate declaration correctness + ReusableAppUtils.registerField(context.getRequiredTestClass(), this, config); } + return config; + } + @Override + protected DropwizardTestSupport prepareTestSupport(final String configPrefix, + final ExtensionContext context, + final List setups) { // setups from @EnableSetup fields go last config.extensions.addAll(setups); - TestSetupUtils.executeSetup(config, context); + TestSetupUtils.executeSetup(config, context, listeners); + final Stopwatch timer = Stopwatch.createStarted(); HooksUtil.register(config.hooks); + tracker.performanceTrack(GuiceyTestTime.HooksRegistration, timer.elapsed()); - return create(configPrefix, config.app, config.configPath, context); + timer.reset().start(); + final DropwizardTestSupport res = create(configPrefix, config.app, config.configPath, context); + tracker.performanceTrack(GuiceyTestTime.DropwizardTestSupport, timer.elapsed()); + return res; } @SuppressWarnings({"unchecked", "checkstyle:Indentation"}) @@ -128,11 +157,19 @@ private DropwizardTestSupport create( final Class app, final String configPath, final ExtensionContext context) { + final C manualConfig = config.getConfiguration(config.configPath); // NOTE: DropwizardTestSupport.ServiceListener listeners would be called ONLY on start! - return new GuiceyTestSupport((Class>) app, + final GuiceyTestSupport support = manualConfig == null + ? new GuiceyTestSupport<>((Class>) app, configPath, configPrefix, - buildConfigOverrides(configPrefix, context)); + buildConfigOverrides(configPrefix, context)) + : new GuiceyTestSupport<>((Class>) app, manualConfig); + support.configModifiers((List>) (List) config.configModifiers); + if (!config.managedLifecycle) { + support.disableManagedLifecycle(); + } + return support; } @SuppressWarnings("unchecked") @@ -149,10 +186,17 @@ private ConfigOverride[] buildCo /** * Builder used for manual extension registration ({@link #forApp(Class)}). + * + * @param configuration type (resolved automatically by application clas */ - public static class Builder extends ExtensionBuilder { + public static class Builder extends ExtensionBuilder, Config> { - public Builder(final Class app) { + /** + * Create builder. + * + * @param app application type + */ + public Builder(final Class> app) { super(new Config()); this.cfg.app = Preconditions.checkNotNull(app, "Application class must be provided"); } @@ -163,7 +207,7 @@ public Builder(final Class app) { * @param configPath configuration file path * @return builder instance for chained calls */ - public Builder config(final String configPath) { + public Builder config(final String configPath) { cfg.configPath = configPath; return this; } @@ -187,7 +231,7 @@ public Builder config(final String configPath) { * @return builder instance for chained calls */ @SafeVarargs - public final Builder setup(final Class... support) { + public final Builder setup(final Class... support) { cfg.extensionClasses(support); return this; } @@ -209,11 +253,28 @@ public final Builder setup(final Class... suppor * @param support support object instances * @return builder instance for chained calls */ - public Builder setup(final TestEnvironmentSetup... support) { + public Builder setup(final TestEnvironmentSetup... support) { cfg.extensionInstances(support); return this; } + /** + * By default, guicey simulates {@link io.dropwizard.lifecycle.Managed} and + * {@link org.eclipse.jetty.util.component.LifeCycle} lifecycle. + *

        + * It might be required in test to avoid starting managed objects (especially all managed in application) + * because important (for test) services replaced with mocks (and no need to wait for the rest of the + * application). + *

        + * Warning: some guice reports might stop working, because they rely on application start event + * + * @return builder instance for chained calls + */ + public Builder disableManagedLifecycle() { + cfg.managedLifecycle = false; + return this; + } + /** * Creates extension. *

        @@ -230,10 +291,11 @@ public TestGuiceyAppExtension create() { /** * Unified configuration. */ - @SuppressWarnings({"checkstyle:VisibilityModifier", "PMD.DefaultPackage"}) + @SuppressWarnings("checkstyle:VisibilityModifier") private static class Config extends ExtensionConfig { Class app; String configPath = ""; + boolean managedLifecycle = true; Config() { super(new TestExtensionsTracker()); @@ -265,9 +327,19 @@ static Config parse(final TestGuiceyApp ann, final TestExtensionsTracker tracker res.app = ann.value(); res.configPath = ann.config(); res.configOverrides = ann.configOverride(); + res.configModifiersFromAnnotation(ann.annotationType(), ann.configModifiers()); res.hooksFromAnnotation(ann.annotationType(), ann.hooks()); res.extensionsFromAnnotation(ann.annotationType(), ann.setup()); + res.injectOnce = ann.injectOnce(); res.tracker.debug = ann.debug(); + res.reuseApp = ann.reuseApplication(); + res.defaultExtensionsEnabled = ann.useDefaultExtensions(); + if (ann.apacheClient()) { + res.clientFactory(ApacheTestClientFactory.class); + } else { + res.clientFactory(ann.clientFactory()); + } + res.managedLifecycle = ann.managedLifecycle(); return res; } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java similarity index 90% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java index 9f8f0832c..17c005424 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/TestParametersSupport.java @@ -6,9 +6,10 @@ import com.google.inject.BindingAnnotation; import com.google.inject.Injector; import com.google.inject.Key; -import io.dropwizard.Application; -import io.dropwizard.Configuration; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.inject.Qualifier; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; @@ -18,7 +19,6 @@ import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.jupiter.param.Jit; -import javax.inject.Qualifier; import java.lang.annotation.Annotation; import java.lang.reflect.Parameter; import java.util.List; @@ -30,6 +30,8 @@ *

      • {@link Application} or exact application class
      • *
      • {@link ObjectMapper}
      • *
      • {@link ClientSupport} application web client helper
      • + *
      • {@link DropwizardTestSupport} support object itself
      • + *
      • {@link ExtensionContext} junit extension context
      • *
      • Any existing guice binding (possibly with qualifier annotation or generified)
      • *
      • {@link Jit} annotated parameter will be obtained from guice context (assume JIT binding)
      • * @@ -42,7 +44,9 @@ public abstract class TestParametersSupport implements ParameterResolver { private final List> supportedClasses = ImmutableList.of( ObjectMapper.class, - ClientSupport.class); + ClientSupport.class, + DropwizardTestSupport.class, + ExtensionContext.class); @Override @SuppressWarnings("checkstyle:ReturnCount") @@ -92,6 +96,12 @@ public Object resolveParameter(final ParameterContext parameterContext, if (ObjectMapper.class.equals(type)) { return support.getObjectMapper(); } + if (DropwizardTestSupport.class.isAssignableFrom(type)) { + return support; + } + if (ExtensionContext.class.isAssignableFrom(type)) { + return extensionContext; + } return InjectorLookup.getInjector(support.getApplication()) .map(it -> it.getInstance(getKey(parameter))) .get(); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClient.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClient.java new file mode 100644 index 000000000..df5359f1a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClient.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Inject web client (derived from {@link ru.vyarus.dropwizard.guice.test.ClientSupport}) into test field. + *
          + *
        • {@code @WebClient ClientSupport client}
        • + *
        • {@code @WebClient(APP) TestClient app} - same as {@code client.appClient()}
        • + *
        • {@code @WebClient(ADMIN) TestClient admin} - same as {@code client.adminClient()}
        • + *
        • {@code @WebClient(REST) TestClient rest} - same as {@code client.restClient()}
        • + *
        + * + * @author Vyacheslav Rusakov + * @since 15.10.2025 + * @see ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient for direct resource client injection + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface WebClient { + + /** + * @return client type (default for root object and other for specific clients) + */ + WebClientType value() default WebClientType.Support; + + /** + * Reset client defaults after each test method. Enabled by default. + * + * @return false to disable defaults reset + */ + boolean autoReset() default true; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientFieldsSupport.java new file mode 100644 index 000000000..1a72f31f6 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientFieldsSupport.java @@ -0,0 +1,93 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.client; + +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; + +import java.util.List; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient} field support implementation. + *

        + * Injects either root {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object or one of specific clients. + * + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class WebClientFieldsSupport extends AnnotatedTestFieldSetup { + + private static final String TEST_CLIENT_FIELDS = "TEST_CLIENT_FIELDS"; + + /** + * Create client fields support. + */ + public WebClientFieldsSupport() { + super(WebClient.class, TestClient.class, TEST_CLIENT_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + final WebClientType type = field.getAnnotation().value(); + if (WebClientType.Support.equals(type) && !ClientSupport.class.equals(field.getType())) { + throw new IllegalStateException("ClientSupport type must be used for the default @WebClient field: " + + field.toStringField()); + } + } + + @Override + protected void registerHooks(final TestExtension extension) { + // nothing + } + + @Override + protected void initializeField(final AnnotatedField field, + final TestClient userValue) { + // nothing + } + + @Override + protected void beforeValueInjection(final EventContext context, + final AnnotatedField field) { + // nothing + } + + @Override + protected TestClient injectFieldValue(final EventContext context, + final AnnotatedField field) { + final ClientSupport support = context.getClient(); + return switch (field.getAnnotation().value()) { + case Support -> support; + case App -> support.appClient(); + case Admin -> support.adminClient(); + case Rest -> support.restClient(); + }; + } + + @Override + protected void report(final EventContext context, + final List> annotatedFields) { + // no reports required for web client fields + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, + final TestClient value) { + // not used + } + + @Override + protected void afterTest(final EventContext context, + final AnnotatedField field, + final TestClient value) { + // reset client defaults + if (field.getAnnotation().autoReset()) { + value.reset(); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientType.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientType.java new file mode 100644 index 000000000..59efcbec2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/WebClientType.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.client; + +/** + * Client type for {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient} annotation. + * + * @author Vyacheslav Rusakov + * @since 15.10.2025 + */ +public enum WebClientType { + /** + * Root {@link ru.vyarus.dropwizard.guice.test.ClientSupport} client (used to obtain other clients). + * This is also a client, targeted application root (may be different from actual application mapping). + */ + Support, + /** + * Application client ({@link ru.vyarus.dropwizard.guice.test.ClientSupport#appClient()}). + */ + App, + /** + * Admin client ({@link ru.vyarus.dropwizard.guice.test.ClientSupport#adminClient()}). + */ + Admin, + /** + * Rest client ({@link ru.vyarus.dropwizard.guice.test.ClientSupport#restClient()}). + */ + Rest +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClient.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClient.java new file mode 100644 index 000000000..7716d5add --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClient.java @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Inject client for exact resource class. The same as {@code ClientSupport.restClient(Resource.class)}. + *

        + * Usage: {@code @WebResourceClient ResourceClient rest}. + *

        + * The same could be achieved with the root rest client (or client support object): + *

        {@code @WebClient(REST) TestClient rest;
        + *  ResourceClient res = rest.restClient(Resource.class)
        + * }
        + *

        + * Works with both integration tests and stub rest. + * + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface WebResourceClient { + + /** + * Reset client defaults after each test method. Enabled by default. + * + * @return false to disable defaults reset + */ + boolean autoReset() default true; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClientFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClientFieldsSupport.java new file mode 100644 index 000000000..6e0111824 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/client/rest/WebResourceClientFieldsSupport.java @@ -0,0 +1,97 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest; + +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.RestStubFieldsSupport; + +import java.util.List; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient} field support implementation. + *

        + * Works with both integration tests and stubs rest. + * + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class WebResourceClientFieldsSupport extends AnnotatedTestFieldSetup { + private static final String TEST_REST_CLIENT_FIELDS = "TEST_REST_CLIENT_FIELDS"; + + /** + * Create resource client fields support. + */ + public WebResourceClientFieldsSupport() { + super(WebResourceClient.class, ResourceClient.class, TEST_REST_CLIENT_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + final Class generic = field.getTypeParameters().get(0); + if (Object.class.equals(generic)) { + throw new IllegalStateException("Target resource class must be specified" + + " as generic (ResourceClient) in field: " + field.toStringField()); + } + } + + @Override + protected void registerHooks(final TestExtension extension) { + // nothing + } + + @Override + protected void initializeField(final AnnotatedField field, + final ResourceClient userValue) { + // nothing + } + + @Override + protected void beforeValueInjection(final EventContext context, + final AnnotatedField field) { + // not used + } + + @Override + protected ResourceClient injectFieldValue(final EventContext context, + final AnnotatedField field) { + final Class resource = field.getTypeParameters().get(0); + if (context.isWebStarted()) { + // integration test + return context.getClient().restClient(resource); + } else { + // rest stubs test + return RestStubFieldsSupport.lookupRestClient(context.getJunitContext()) + .orElseThrow(() -> new IllegalStateException(String.format( + "Resource client can't be used under lightweight guicey test without @StubRest: " + + field.toStringField()))) + .restClient(resource); + } + } + + @Override + protected void report(final EventContext context, + final List> annotatedFields) { + // not needed + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, + final ResourceClient value) { + // nothing + } + + @Override + protected void afterTest(final EventContext context, + final AnnotatedField field, + final ResourceClient value) { + // reset client defaults + if (field.getAnnotation().autoReset()) { + value.reset(); + } + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java similarity index 52% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java index 584010ea4..707dd80ed 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionBuilder.java @@ -1,8 +1,15 @@ package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf; +import com.google.common.base.Preconditions; +import io.dropwizard.core.Configuration; import io.dropwizard.testing.ConfigOverride; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.api.function.ThrowingSupplier; import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideExtensionValue; import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideValue; @@ -17,18 +24,60 @@ * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} builders (to avoid duplicating * method implementations). * + * @param dropwizard configuration type * @param config object type * @param builder type * @author Vyacheslav Rusakov * @since 12.05.2022 */ -public abstract class ExtensionBuilder { +public abstract class ExtensionBuilder, + C extends ExtensionConfig> { + + /** + * Configuration instance. + */ protected final C cfg; + /** + * Create builder. + * + * @param cfg configuration instance + */ public ExtensionBuilder(final C cfg) { this.cfg = cfg; } + /** + * Custom block to perform manual configurations inside. It is better suited for lambda configurations + * (when builder configured in test field). Also, it captures exceptions (no need for manual try-catch blocks). + * + * @param action action to execute + * @return builder instance for chained calls + */ + public T with(final ThrowingConsumer action) { + try { + action.accept(self()); + } catch (Throwable e) { + throw new IllegalStateException("Test configuration failed", e); + } + return self(); + } + + /** + * Specify configuration instance directly, instead of parsing yaml file. + *

        + * NOTE: Configuration overrides will not work! But configuration modifiers will work. + * + * @param configProvider configuration instance provider + * @return builder instance for chained calls + */ + public T config(final ThrowingSupplier configProvider) { + Preconditions.checkState(cfg.confInstance == null, "Manual configuration instance already set"); + cfg.confInstance = configProvider; + return self(); + } + /** * Specifies configuration overrides pairs in format: {@code "key: value"}. Might be called multiple times * (values appended). @@ -40,6 +89,7 @@ public ExtensionBuilder(final C cfg) { * @return builder instance for chained calls * @see #configOverrides(io.dropwizard.testing.ConfigOverride...) * for using {@link io.dropwizard.testing.ConfigOverride} objects directly + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) */ public T configOverrides(final String... values) { cfg.configOverrides = ConfigOverrideUtils.mergeRaw(cfg.configOverrides, values); @@ -62,6 +112,7 @@ public T configOverrides(final String... values) { * @return builder instance for chained calls * @see ru.vyarus.dropwizard.guice.test.util.ConfigOverrideValue for an exmample of required implementation * @see #configOverride(String, java.util.function.Supplier) for supplier shortcut + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) */ @SafeVarargs public final T configOverrides(final K... values) { @@ -69,6 +120,21 @@ public final T configOverrides(f return self(); } + /** + * Shortcut for config override registration. + *

        + * Note that overrides order is not predictable so don't specify multiple values for the same property + * (see {@link io.dropwizard.testing.DropwizardTestSupport} holds overrides in {@link java.util.Set}). + * + * @param key property name + * @param value property value + * @return builder instance for chained calls + */ + public T configOverride(final String key, final String value) { + configOverrides(key + ":" + value); + return self(); + } + /** * Register config override with a supplier. Useful for values with delayed resolution * (e.g. provided by some other extension). @@ -79,6 +145,7 @@ public final T configOverrides(f * @param key configuration key * @param supplier value supplier * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) */ public T configOverride(final String key, final Supplier supplier) { configOverrides(new ConfigOverrideValue(key, supplier)); @@ -93,6 +160,7 @@ public T configOverride(final String key, final Supplier supplier) { * @param namespace junit storage namespace to resolve value in * @param key value name in namespace and overriding property name * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) */ public T configOverrideByExtension(final ExtensionContext.Namespace namespace, final String key) { return configOverrideByExtension(namespace, key, key); @@ -122,6 +190,7 @@ public T configOverrideByExtension(final ExtensionContext.Namespace namespace, f * @param storageKey value name in namespace * @param configPath overriding property name * @return builder instance for chained calls + * @see #configModifiers(ru.vyarus.dropwizard.guice.test.util.ConfigModifier[]) */ public T configOverrideByExtension(final ExtensionContext.Namespace namespace, final String storageKey, @@ -171,11 +240,70 @@ public T hooks(final GuiceyConfigurationHook... hooks) { return self(); } + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + *

        + * Modifier is called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml and instance-based configurations. + *

        + * Method supposed to be used with lambdas and so limited for application configuration class. + * For generic configurations (based on configuration subclass or raw {@link io.dropwizard.core.Configuration}) + * use {@link #configModifiers(Class[])}. + * + * @param modifiers configuration modifiers + * @param

        configuration type + * @return builder instance for chained calls + */ + @SafeVarargs + // generic required for cases when the configuration type is not provided (env. setup object) + public final

        T configModifiers(final ConfigModifier

        ... modifiers) { + cfg.configModifierInstances(modifiers); + return self(); + } + + /** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + *

        + * Modifier is called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml and instance-based configurations. + *

        + * Method is useful for generic modifiers (based on configuration subclass or raw + * {@link io.dropwizard.core.Configuration}). + * + * @param modifiers configuration modifiers + * @return builder instance for chained calls + */ + @SafeVarargs + public final T configModifiers(final Class>... modifiers) { + cfg.configModifierClasses(modifiers); + return self(); + } + + /** + * When test lifecycle is {@link org.junit.jupiter.api.TestInstance.Lifecycle#PER_CLASS} same test instance + * used for all test methods. By default, guicey would perform fields injection before each method because + * there might be prototype beans that must be refreshed for each test method. If you don't rely on + * prototypes, injections could be performed just once (for the first test method). + * + * @return builder instance for chained calls + */ + public T injectOnce() { + cfg.injectOnce = true; + return self(); + } + /** * Enables debug output for extension: used setup objects, hooks and applied config overrides. Might be useful * for concurrent tests too because each message includes configuration prefix (exactly pointing to context test * or method). *

        + * Also, shows guicey extension time, so if you suspect that guicey spent too much time, use the debug option to + * be sure. Performance report is published after each "before each" phase and after "after all" to let you + * see how extension time increased with each test method (for non-static guicey extension (executed per method), + * performance printed after "before each" and "after each" because before/after all not available) + *

        * Configuration overrides are printed after application startup (but before the test) because overridden values * are resolved from system properties (applied by {@link io.dropwizard.testing.DropwizardTestSupport#before()}). * If application startup failed, no configuration overrides would be printed (because dropwizard would immediately @@ -192,6 +320,71 @@ public T debug() { return self(); } + /** + * By default, a new application instance is started for each test. If you want to re-use the same application + * instance between several tests, then put extension declaration in BASE test class and enable the reuse option: + * all tests derived from this base class would use the same application instance. + *

        + * You may have multiple base classes with reusable application declaration (different test hierarchies) - in + * this case, multiple applications would be kept running during tests execution. + *

        + * All other extensions (without enabled re-use) will start new applications: take this into account to + * prevent port clashes with already started reusable apps. + *

        + * Reused application instance would be stopped after all tests execution. + * + * @return builder instance for chained calls + */ + public T reuseApplication() { + cfg.reuseApp = true; + return self(); + } + + /** + * Default extensions: {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest}. + *

        + * Disables service lookup for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup}. + *

        + * By default, these extensions enabled and this option could disable them (if there are problems with them or + * fields analysis took too much time). + * + * @return builder instance for chained calls + */ + public T disableDefaultExtensions() { + cfg.defaultExtensionsEnabled = false; + return self(); + } + + /** + * Use custom jersey client builder for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} object. + * + * @param factory factory implementation + * @return builder instance for chained calls + */ + public T clientFactory(final TestClientFactory factory) { + cfg.clientFactory = factory; + return self(); + } + + /** + * Shortcut for {@link #clientFactory(ru.vyarus.dropwizard.guice.test.client.TestClientFactory)} to configure + * {@link ru.vyarus.dropwizard.guice.test.client.ApacheTestClientFactory}. The default + * {@link org.glassfish.jersey.client.HttpUrlConnectorProvider} supports only HTTP 1.1 methods and have + * problem with PATCH method usage on jdk > 16. + *

        + * Apache client is not set by default because of its problems with multipart: see + * jersey issue. + * + * @return builder instance for chained calls + */ + public T apacheClient() { + return clientFactory(new ApacheTestClientFactory()); + } @SuppressWarnings("unchecked") private T self() { diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java new file mode 100644 index 000000000..917875c45 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java @@ -0,0 +1,218 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf; + +import com.google.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.ConfigOverride; +import org.junit.jupiter.api.function.ThrowingSupplier; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.test.client.TestClientFactory; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.TestExtensionsTracker; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.HooksUtil; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Base configuration for junit 5 extensions (contains common configurations). Required to unify common configuration + * methods in {@link ExtensionBuilder}. + * + * @author Vyacheslav Rusakov + * @since 12.05.2022 + */ +@SuppressWarnings({"checkstyle:VisibilityModifier", "PMD.AvoidFieldNameMatchingMethodName"}) +public abstract class ExtensionConfig { + + /** + * Configuration overrides. + */ + public String[] configOverrides = new String[0]; + /** + * Configuration instance supplier. + */ + public ThrowingSupplier confInstance; + /** + * Configuration override object. Required for lazy evaluation values + */ + public final List configOverrideObjects = new ArrayList<>(); + /** + * Configuration modifiers. + */ + public final List> configModifiers = new ArrayList<>(); + /** + * Hooks. + */ + public final List hooks = new ArrayList<>(); + /** + * Setup objects. + */ + public final List extensions = new ArrayList<>(); + /** + * Client factory. + */ + public TestClientFactory clientFactory; + /** + * Inject test fields once. + */ + public boolean injectOnce; + /** + * Service lookup for extensions enabled. + */ + public boolean defaultExtensionsEnabled = true; + /** + * Extension registration source tracker (tracks source of registered setup objects). + */ + public final TestExtensionsTracker tracker; + + /** + * Reuse application instance between tests. + */ + public boolean reuseApp; + /** + * Test class where reuse was declared. + */ + public Class reuseDeclarationClass; + /** + * Description of declaration field or annotation (in declaration class). + */ + public String reuseSource; + + /** + * Create config. + * + * @param tracker tracker + */ + public ExtensionConfig(final TestExtensionsTracker tracker) { + this.tracker = tracker; + } + + + /** + * Register extensions declared in annotation. + * + * @param ann annotation type + * @param exts extensions + */ + @SafeVarargs + public final void extensionsFromAnnotation(final Class ann, + final Class... exts) { + extensions.addAll(TestSetupUtils.create(exts)); + tracker.extensionsFromAnnotation(ann, exts); + } + + /** + * Register hooks declared in annotation. + * + * @param ann annotation type + * @param exts hooks + */ + @SafeVarargs + public final void hooksFromAnnotation(final Class ann, + final Class... exts) { + hooks.addAll(HooksUtil.create(exts)); + tracker.hooksFromAnnotation(ann, exts); + } + + /** + * Register hooks by instance (declared in field extension). + * + * @param exts hooks + */ + public final void hookInstances(final GuiceyConfigurationHook... exts) { + Collections.addAll(hooks, exts); + tracker.hookInstances(exts); + } + + /** + * Register hook classes (declared in field extension). + * + * @param exts hooks + */ + @SafeVarargs + public final void hookClasses(final Class... exts) { + hooks.addAll(HooksUtil.create(exts)); + tracker.hookClasses(exts); + } + + /** + * Register configuration modifiers from annotation. + * + * @param ann annotation type + * @param modifiers modifiers + */ + @SafeVarargs + public final void configModifiersFromAnnotation(final Class ann, + final Class>... modifiers) { + configModifiers.addAll(ConfigOverrideUtils.createModifiers(modifiers)); + tracker.configModifiersFromAnnotation(ann, modifiers); + } + + /** + * Register configuration modifier classes (declared in field extension). + * + * @param modifiers modifiers + */ + @SafeVarargs + public final void configModifierClasses(final Class>... modifiers) { + configModifiers.addAll(ConfigOverrideUtils.createModifiers(modifiers)); + tracker.configModifierClasses(modifiers); + } + + /** + * Register configuration modifiers declared as instances (in field extension). + * + * @param modifiers modifiers + */ + public final void configModifierInstances(final ConfigModifier... modifiers) { + Collections.addAll(configModifiers, modifiers); + tracker.configModifierInstances(modifiers); + } + + /** + * @param factoryType client factory class + */ + @SuppressFBWarnings("PA_PUBLIC_PRIMITIVE_ATTRIBUTE") + public final void clientFactory(final Class factoryType) { + try { + this.clientFactory = InstanceUtils.create(factoryType); + } catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate test client factory", ex); + } + } + + /** + * Obtain manual configuration instance with validation (a file should not be configured). + * + * @param configPath configuration file path (optional) + * @param configuration type + * @return manual configuration instance + */ + @SuppressWarnings("unchecked") + public C getConfiguration(final String configPath) { + C cfg = null; + if (confInstance != null) { + Preconditions.checkState(configPath.isEmpty(), + "Configuration path can't be used with manual configuration instance: %s", configPath); + Preconditions.checkState(configOverrides.length == 0, + "Configuration overrides can't be used with manual configuration instance: %s", + Arrays.toString(configOverrides)); + Preconditions.checkState(configOverrideObjects.isEmpty(), + "Configuration overrides can't be used with manual configuration instance"); + try { + cfg = (C) confInstance.get(); + } catch (Throwable e) { + throw new IllegalStateException("Manual configuration instance construction failed", e); + } + Preconditions.checkNotNull(cfg, "Configuration can't be null"); + } + return cfg; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/GuiceyTestTime.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/GuiceyTestTime.java new file mode 100644 index 000000000..d166b2c50 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/GuiceyTestTime.java @@ -0,0 +1,80 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track; + +/** + * Guicey timers for test extensions performance measurement. + *

        + * Note that actions may be performed under different test phases (e.g., application could start in before all + * for all test methods, but could be in before each for per-method instance tests). + * + * @author Vyacheslav Rusakov + * @since 04.02.2025 + */ +public enum GuiceyTestTime { + /** + * Before all time. + */ + BeforeAll("Before all"), + /** + * Before each time. + */ + BeforeEach("Before each"), + /** + * After each time. + */ + AfterEach("After each"), + /** + * After all time. + */ + AfterAll("After all"), + + /** + * Guice injectMemebers() executed for test instance. + */ + GuiceInjection("Guice fields injection"), + /** + * Hook and setup fields searched to verify correctness (different from fields analysis below). + */ + ReusableAppWarnings("Check reusable app warnings"), + /** + * Guicey hook and setup fields found and resolved (reflection). + */ + GuiceyFieldsSearch("Guicey fields search"), + /** + * Registration of hooks resolved from fields and declared in extension (application hooks registration + * not tracked). + */ + HooksRegistration("Guicey hooks registration"), + /** + * Setup objects executed. + */ + SetupObjectsExecution("Guicey setup objects execution"), + /** + * Creation of the dropwizard or guicey test support object. + */ + DropwizardTestSupport("DropwizardTestSupport creation"), + /** + * Test support object before() executed (application start plus configuration overrides). + */ + SupportStart("Application start"), + /** + * Test support after() executed (application stopped). + */ + SupportStop("Application stop"), + /** + * Test listeners time (registered in setup objects). + */ + TestListeners("Listeners execution"); + + private final String name; + + GuiceyTestTime(final String name) { + this.name = name; + } + + /** + * @return display name + */ + public String getDisplayName() { + return name; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/PerformanceTrack.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/PerformanceTrack.java new file mode 100644 index 000000000..1b1b3cefd --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/PerformanceTrack.java @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track; + +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.time.Duration; + +/** + * Depending on test, application could be instantiated before all or before each test methods. + * When executing methods of the same test, indicating increased time (e.g., each beforeEach). + */ +@SuppressWarnings("VisibilityModifier") +class PerformanceTrack { + final GuiceyTestTime name; + final GuiceyTestTime phase; + // for simplicity, tracking increase between logs (that's what we actually need) + Duration loggedDuration; + Duration duration; + + PerformanceTrack(final GuiceyTestTime name, final GuiceyTestTime phase) { + this.name = name; + this.phase = phase; + } + + void registerDuration(final Duration duration) { + this.duration = this.duration == null ? duration : this.duration.plus(duration); + } + + boolean isDurationChanged() { + return loggedDuration == null || loggedDuration.compareTo(duration) < 0; + } + + void markLogged() { + loggedDuration = duration; + } + + boolean isRoot() { + return phase == name; + } + + Duration getOverall() { + return duration; + } + + Duration getIncrease() { + return loggedDuration == null ? duration : duration.minus(loggedDuration); + } + + @Override + public String toString() { + String title = name.getDisplayName(); + if (isRoot()) { + title = "[" + title + "]"; + } + return String.format("%-35s: %s", title, PrintUtils.renderTime(getOverall(), + loggedDuration == null ? null : getIncrease())); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/RegistrationTrackUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/RegistrationTrackUtils.java new file mode 100644 index 000000000..f444f2b3b --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/RegistrationTrackUtils.java @@ -0,0 +1,115 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track; + +import com.google.common.collect.ImmutableList; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.StackUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +/** + * Utilities for rendering registration tracking info for hooks and setup test objects. + * + * @author Vyacheslav Rusakov + * @since 20.05.2022 + */ +@SuppressWarnings("PMD.UseVarargs") +public final class RegistrationTrackUtils { + + private static final List> EXT_INFRA = ImmutableList.of( + ExtensionBuilder.class, + TestExtensionsTracker.class, + ExtensionConfig.class, + RegistrationTrackUtils.class, + TestGuiceyAppExtension.class, + TestDropwizardAppExtension.class); + + private RegistrationTrackUtils() { + } + + /** + * Stores tracking info for registered classes. + * + * @param info info holder + * @param prefix source identity + * @param classes items to append + * @param fromAnnotation indicates class declaration in annotation + */ + public static void fromClass(final List info, final String prefix, final Class[] classes, + final boolean fromAnnotation) { + // it is not possible to detect the source file by annotation here + final String src = fromAnnotation ? prefix : buildSourceLocation(prefix); + track(info, Arrays.asList(classes), it -> it, it -> src); + } + + /** + * Stores tracking info for registered instances. + * + * @param info info holder + * @param prefix source identity + * @param instances instances to append + */ + public static void fromInstance(final List info, final String prefix, final Object[] instances) { + final String src = buildSourceLocation(prefix); + track(info, Arrays.asList(instances), Object::getClass, obj -> src); + } + + /** + * Store tracking info for recognized test class fields. + * + * @param info info holder + * @param prefix source identity + * @param fields fields to append + * @param instance test instance or null for static fields + */ + public static void fromField(final List info, + final String prefix, + final List> fields, + final Object instance) { + track(info, fields, + field -> field.getValue(instance).getClass(), + field -> formatSourceLocation(prefix + " " + getFieldDescriptor(field), + "at " + RenderUtils.renderClass(field.getDeclaringClass()) + "#" + field.getName() + )); + } + + /** + * @param field field + * @return field description string + */ + public static String getFieldDescriptor(final AnnotatedField field) { + return RenderUtils.getClassName(field.getDeclaringClass()) + "#" + field.getName(); + } + + private static String buildSourceLocation(final String prefix) { + final String source = StackUtils.getCallerSource(EXT_INFRA); + return formatSourceLocation(prefix, source); + } + + private static String formatSourceLocation(final String prefix, final String src) { + return String.format("%-50s %s", prefix, src); + } + + private static void track(final List info, + final List objects, + final Function converter, + final Function marker) { + for (T obj : objects) { + final Class cls = converter.apply(obj); + final String className; + // avoid showing ugly not informative class names for anonymous and lambda classes + if (cls.isAnonymousClass() || cls.isSynthetic()) { + className = ""; + } else { + className = RenderUtils.getClassName(cls); + } + info.add(String.format("%-30s \t%s", className, marker.apply(obj))); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TestExtensionsTracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TestExtensionsTracker.java new file mode 100644 index 000000000..09da0260a --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TestExtensionsTracker.java @@ -0,0 +1,333 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; + +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tracks registration of hooks and support objects during test initialization in order to log used + * additions (to simplify applied objects tracking). Also, tracks applied configuration overrides, but only after + * application start (the only way to show actually applied values). + *

        + * Also, tracks guicey extensions performance to simplify test performance problems resolution. + * + * @author Vyacheslav Rusakov + * @since 27.05.2022 + */ +public class TestExtensionsTracker { + + /** + * System property enables debug output for all used guicey extensions. + */ + public static final String GUICEY_EXTENSIONS_DEBUG = "guicey.extensions.debug"; + /** + * Enabled value for {@link #GUICEY_EXTENSIONS_DEBUG} system property. + */ + public static final String DEBUG_ENABLED = "true"; + + /** + * Extension debug state. + */ + @SuppressWarnings("checkstyle:VisibilityModifier") + public boolean debug; + + /** + * Setup objects registration sources. + */ + protected final List extensionsSource = new ArrayList<>(); + /** + * Hooks registration sources. + */ + protected final List hooksSource = new ArrayList<>(); + /** + * Configuration modifiers registration sources. + */ + protected final List configModifierSource = new ArrayList<>(); + private final List performance = new ArrayList<>(); + + private GuiceyTestTime testPhase; + private Class contextSetupObject; + // setup object in field could be a lambda, and it could register hook with lambda - need to remember field name + // to provide meaningful registration context in the report + private final Map, String> fieldSetupObjectsReference = new HashMap<>(); + + /** + * @param setup context test extension + */ + public void setContextSetupObject(final Class setup) { + contextSetupObject = setup; + } + + /** + * Register setup objects from test fields. + * + * @param fields fields + * @param instance test instance + */ + @SuppressWarnings("unchecked") + public final void extensionsFromFields(final List> fields, + final Object instance) { + final String prefix = "@" + EnableSetup.class.getSimpleName(); + RegistrationTrackUtils.fromField(extensionsSource, prefix, + (List>) (List) fields, instance); + // store meaningful names to clearly identify lambda hook registration source + fields.forEach(field -> fieldSetupObjectsReference + .put(field.getCachedValue().getClass(), prefix + " " + + RegistrationTrackUtils.getFieldDescriptor(field))); + } + + /** + * Register setup objects from annotation. + * + * @param ann annotation type + * @param exts setup objects + */ + @SafeVarargs + public final void extensionsFromAnnotation(final Class ann, + final Class... exts) { + // sync actual extension registration order with tracking info + final List tmp = new ArrayList<>(extensionsSource); + extensionsSource.clear(); + RegistrationTrackUtils.fromClass(extensionsSource, "@" + ann.getSimpleName() + "(setup)", exts, true); + extensionsSource.addAll(tmp); + } + + /** + * Register hooks from test fields. + * + * @param fields fields + * @param baseHooks hooks from base class + * @param instance test instance + */ + @SuppressWarnings("unchecked") + public final void hooksFromFields(final List> fields, + final boolean baseHooks, + final Object instance) { + if (!fields.isEmpty()) { + // hooks from fields in base classes activated before configured hooks + final List tmp = baseHooks ? new ArrayList<>(hooksSource) : Collections.emptyList(); + if (baseHooks) { + hooksSource.clear(); + } + RegistrationTrackUtils.fromField(hooksSource, "@" + EnableHook.class.getSimpleName(), + (List>) (List) fields, instance); + hooksSource.addAll(tmp); + } + } + + /** + * Register hooks from annotation. + * + * @param ann annotation type + * @param exts hooks + */ + @SafeVarargs + public final void hooksFromAnnotation(final Class ann, + final Class... exts) { + RegistrationTrackUtils.fromClass(hooksSource, "@" + ann.getSimpleName() + "(hooks)", exts, true); + } + + /** + * Register setup objects instances. + * + * @param exts setup objects + */ + public final void extensionInstances(final TestEnvironmentSetup... exts) { + RegistrationTrackUtils.fromInstance(extensionsSource, String.format("@%s.setup(obj)", + RegisterExtension.class.getSimpleName()), exts); + } + + /** + * Register setup objects classes. + * + * @param exts setup object classes + */ + @SafeVarargs + public final void extensionClasses(final Class... exts) { + RegistrationTrackUtils.fromClass(extensionsSource, String.format("@%s.setup(class)", + RegisterExtension.class.getSimpleName()), exts, false); + } + + /** + * Register setup objects resolved with service loader. + * + * @param exts setup objects + * @return setup object instances + */ + @SafeVarargs + public final List lookupExtensions(final TestEnvironmentSetup... exts) { + RegistrationTrackUtils.fromInstance(extensionsSource, "lookup (service loader)", exts); + return Arrays.asList(exts); + } + + /** + * Register default setup objects (which must be registered last due to AOP usage). + * + * @param exts setup objects + * @return setup objects instances + */ + @SafeVarargs + public final List defaultExtensions(final TestEnvironmentSetup... exts) { + RegistrationTrackUtils.fromInstance(extensionsSource, "default extension", exts); + return Arrays.asList(exts); + } + + /** + * Register hook instances. + * + * @param exts hooks + */ + public final void hookInstances(final GuiceyConfigurationHook... exts) { + RegistrationTrackUtils.fromInstance(hooksSource, String.format("%s.hooks(obj)", getHookContext()), exts); + } + + /** + * Register hook classes. + * + * @param exts hooks + */ + @SafeVarargs + public final void hookClasses(final Class... exts) { + RegistrationTrackUtils.fromClass(hooksSource, String.format("%s.hooks(class)", getHookContext()), exts, false); + } + + /** + * Register config modifiers from annotation. + * + * @param ann annotation type + * @param exts modifiers + */ + @SafeVarargs + public final void configModifiersFromAnnotation(final Class ann, + final Class... exts) { + RegistrationTrackUtils.fromClass(configModifierSource, "@" + ann.getSimpleName() + "(configModifiers)", + exts, true); + } + + /** + * Register config modifier classes. + * + * @param mods modifiers + */ + @SafeVarargs + public final void configModifierClasses(final Class... mods) { + RegistrationTrackUtils.fromClass(configModifierSource, String.format("%s.configModifiers(class)", + getHookContext()), mods, false); + } + + /** + * Register config modifier instances. + * + * @param exts modifiers + */ + public final void configModifierInstances(final ConfigModifier... exts) { + RegistrationTrackUtils.fromInstance(configModifierSource, String.format("%s.configModifiers(obj)", + getHookContext()), exts); + } + + /** + * Indicate current phase. + * + * @param context junit context + * @param phase phase + */ + public void lifecyclePhase(final ExtensionContext context, final GuiceyTestTime phase) { + testPhase = phase; + } + + /** + * Record performance. + * + * @param name tracker name + * @param duration duration + */ + public void performanceTrack(final GuiceyTestTime name, final Duration duration) { + PerformanceTrack track = performance.stream() + .filter(tr -> tr.phase == testPhase && tr.name == name) + .findFirst().orElse(null); + if (track == null) { + track = new PerformanceTrack(name, testPhase); + performance.add(track); + } + track.registerDuration(duration); + } + + /** + * In some cases it might be simpler to use system property to enable debug: {@code -Dguicey.extensions.debug=true}. + */ + @SuppressFBWarnings("PA_PUBLIC_PRIMITIVE_ATTRIBUTE") + public void enableDebugFromSystemProperty() { + if (!debug && DEBUG_ENABLED.equalsIgnoreCase(System.getProperty(GUICEY_EXTENSIONS_DEBUG))) { + debug = true; + } + } + + /** + * Logs registered setup objects and hooks. Do nothing if no setup objects or hooks registered. + * + * @param configPrefix configuration prefix + */ + @SuppressWarnings("PMD.SystemPrintln") + public void logUsedHooksAndSetupObjects(final String configPrefix) { + if (debug && (!extensionsSource.isEmpty() || !hooksSource.isEmpty())) { + System.out.println(TrackerReportBuilder.buildSetupReport(configPrefix, extensionsSource, hooksSource)); + } + } + + /** + * Logs overridden configurations. Show values already applied to system properties. + * + * @param configPrefix configuration prefix + */ + @SuppressWarnings("PMD.SystemPrintln") + public void logOverriddenConfigs(final String configPrefix) { + if (debug) { + System.out.println(TrackerReportBuilder.buildConfigsReport(configPrefix, configModifierSource)); + } + } + + /** + * Record guicey time. + * + * @param phase phase + * @param context junit context + */ + @SuppressWarnings("PMD.SystemPrintln") + public void logGuiceyTestTime(final GuiceyTestTime phase, final ExtensionContext context) { + if (debug) { + System.out.println(TrackerReportBuilder.buildPerformanceReport(performance, context, phase)); + } + } + + private String getHookContext() { + // hook might be registered from manual extension in filed or within setup object and in this case + // tracking setup object class + String res; + if (contextSetupObject != null) { + // special case: hook registartion under setup object from @EnableSetup field (both could be lambda) + res = fieldSetupObjectsReference.get(contextSetupObject); + if (res == null) { + res = RenderUtils.getClassName(contextSetupObject); + } + } else { + res = "@" + RegisterExtension.class.getSimpleName(); + } + return res; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TrackerReportBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TrackerReportBuilder.java new file mode 100644 index 000000000..09f8a5954 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/track/TrackerReportBuilder.java @@ -0,0 +1,129 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track; + +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * Guicey test extensions debug reports builder. + * + * @author Vyacheslav Rusakov + * @since 07.03.2025 + */ +public final class TrackerReportBuilder { + + private TrackerReportBuilder() { + } + + /** + * Render hooks and setup objects report. + * + * @param configPrefix configuration override prefix + * @param setups setup objects sources + * @param hooks hooks sources + * @return rendered report + */ + public static String buildSetupReport(final String configPrefix, + final List setups, + final List hooks) { + // using config prefix to differentiate outputs for parallel execution + final StringBuilder res = new StringBuilder(500).append("\nGuicey test extensions (") + .append(configPrefix).append(".):\n\n"); + if (!setups.isEmpty()) { + res.append("\tSetup objects = \n"); + logTracks(res, setups); + } + + if (!hooks.isEmpty()) { + res.append("\tTest hooks = \n"); + logTracks(res, hooks); + } + return res.toString(); + } + + /** + * Render performance report. + * + * @param tracks performance tracks + * @param context junit context + * @param phase current phase + * @return rendered report + */ + public static String buildPerformanceReport(final List tracks, + final ExtensionContext context, + final GuiceyTestTime phase) { + final StringBuilder res = new StringBuilder(); + Duration overall = Duration.ZERO; + Duration increase = Duration.ZERO; + for (PerformanceTrack root : tracks) { + if (!root.isRoot()) { + continue; + } + overall = overall.plus(root.getOverall()); + if (root.isDurationChanged()) { + increase = increase.plus(root.getIncrease()); + res.append("\n\t").append(root).append('\n'); + + for (PerformanceTrack track : tracks) { + if (track.isRoot() || track.phase != root.name) { + continue; + } + if (root.isDurationChanged()) { + res.append("\t\t").append(track).append('\n'); + } + } + } + } + + // merge increase delta to start tracking new increases + tracks.forEach(PerformanceTrack::markLogged); + + final Duration lastOverall = overall.minus(increase); + + final String title = PrintUtils.getPerformanceReportSeparator(context) + + "Guicey time after [" + phase.getDisplayName() + "] of " + + TestSetupUtils.getContextTestName(context) + + ": " + PrintUtils.renderTime(overall, + lastOverall.equals(Duration.ZERO) ? null : increase); + + return title + "\n" + res; + } + + /** + * Render configuration modifications report. + * + * @param configPrefix config override prefix + * @param modifiers modifiers + * @return rendered report + */ + public static String buildConfigsReport(final String configPrefix, final List modifiers) { + final StringBuilder res = new StringBuilder(100); + for (Map.Entry entry : System.getProperties().entrySet()) { + final String key = (String) entry.getKey(); + if (key.startsWith(configPrefix)) { + res.append(String.format("\t %20s = %s%n", + key.substring(configPrefix.length() + 1), entry.getValue())); + } + } + + final boolean hasOverrides = !res.isEmpty(); + + if (!modifiers.isEmpty()) { + res.append("\nConfiguration modifiers:\n"); + logTracks(res, modifiers); + } + + return (hasOverrides ? "\nConfiguration overrides (" + configPrefix + ".):\n" : "") + res; + } + + private static void logTracks(final StringBuilder res, final List tracks) { + for (String st : tracks) { + res.append("\t\t").append(st).append('\n'); + } + res.append('\n'); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/LogFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/LogFieldsSupport.java new file mode 100644 index 000000000..c93d59586 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/LogFieldsSupport.java @@ -0,0 +1,126 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.log; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.log.RecordLogsHook; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs} test fields support implementation. + *

        + * Applies custom appenders into required logback loggers. If required, lower loggers level (to receive all required + * events). Note that appender applied three times: before application starts, before init (because app creation reset + * logs) and in dropwizard run phase (because dropwizard would reset loggers during startup). + *

        + * By default, collected logs cleared after each test method. + * + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class LogFieldsSupport extends AnnotatedTestFieldSetup { + private static final String TEST_LOGS_FIELDS = "TEST_LOGS_FIELDS"; + private static final String FIELD_RECORDER = "FIELD_RECORDER"; + + private final RecordLogsHook hook = new RecordLogsHook(); + + /** + * Create support. + */ + public LogFieldsSupport() { + super(RecordLogs.class, RecordedLogs.class, TEST_LOGS_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + // recorder initialization + final RecordLogs config = field.getAnnotation(); + final RecordedLogs logs = hook.record() + .loggers(config.value()) + .loggers(config.loggers()) + // clearly identify appender using field name (for debugging purposes) + .recorderName(field.toStringField()) + .start(config.level()); + field.setCustomData(FIELD_RECORDER, logs); + } + + @Override + + protected void registerHooks(final TestExtension extension) { + extension.hooks(hook); + } + + @Override + protected void initializeField(final AnnotatedField field, + final RecordedLogs userValue) { + // no need + } + + @Override + protected void beforeValueInjection(final EventContext context, + final AnnotatedField field) { + // no need + } + + @Override + protected RecordedLogs injectFieldValue(final EventContext context, + final AnnotatedField field) { + // use a custom object for logs selectors + return Preconditions.checkNotNull(field.getCustomData(FIELD_RECORDER)); + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void report(final EventContext context, + final List> annotatedFields) { + final StringBuilder report = new StringBuilder("\nApplied log recorders (@") + .append(RecordLogs.class.getSimpleName()).append(") on ").append(setupContextName).append("\n\n"); + fields.forEach(field -> { + final RecordLogs config = field.getAnnotation(); + final List loggers = new ArrayList<>(); + Arrays.stream(config.value()).map(Class::getSimpleName).forEach(loggers::add); + Collections.addAll(loggers, config.loggers()); + report.append( + String.format("\t%-30s %-6s %s%n", '#' + field.getField().getName(), config.level(), + String.join(",", loggers)) + ); + }); + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, final RecordedLogs value) { + // no need + } + + @Override + protected void afterTest(final EventContext context, + final AnnotatedField field, final RecordedLogs value) { + if (field.getAnnotation().autoReset()) { + value.clear(); + } + } + + @Override + public void stopped(final EventContext context) { + // last moment before field state would be cleared + fields.forEach(field -> { + final RecordedLogs logs = field.getCustomData(FIELD_RECORDER); + if (logs != null) { + // detach logger (to not collect stale appenders in the root logger) + logs.getRecorder().destroy(); + } + }); + super.stopped(context); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/RecordLogs.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/RecordLogs.java new file mode 100644 index 000000000..e8822460d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/log/RecordLogs.java @@ -0,0 +1,82 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.log; + +import org.slf4j.event.Level; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Record log events for verification. + * IMPORTANT: works ONLY with logback (would be useless with another logger). + *

        + * Without additional configuration would record all events (from the root logger): + * {@code @RecordLogs RecordedLogs logs}. + * In most cases, it would be more convenient to listen to the exact logger logs: + * {@code @RecordLogs(Service.class) RecordedLogs logs} - listen Service logs (assuming logger created as + * {@code LoggerFactory.getLogger(Service.class)}). + * Entire packages could be listened with: {@code @RecordLogs(listeners = "com.package") RecordedLogs logs}. + * (class and string loggers could be specified together). + *

        + * By default, listen WARN logs and above. To set a different level use + * {@code @RecordLogs(value = Service.class level = Level.INFO) RecordedLogs logs}. + * NOTE that logger level would be decreased (re-configured) to receive events from the required threshold. + *

        + * Could be used for a quick logger configuration changes in tests (easy switch to TRACE, for example). + *

        + * Recorded events could be inspected with {@link ru.vyarus.dropwizard.guice.test.log.RecordedLogs} object: + * {@code logs.getEvents()} for raw event objects or {@code logs.getMessages()} for logged messages. There are many + * other methods to filter events. + *

        + * Events recorded for the entire application startup. Dropwizard resets loggers two times: in application constructor + * and just before the run phase (log configuration factory init), so logs listener appender have to be re-registered. + * LIMITATION: would not see run phase logs of dropwizard bundles, registered BEFORE + * {@link ru.vyarus.dropwizard.guice.GuiceBundle} (no way re-attach listener before it). For dropwizard bundles, + * registered after guice bundle (or inside it) - all logs would be visible. + *

        + * Recorded logs are cleared after each test. Use {@link #autoReset()} to disable. Also, clean could be performed + * manually with {@link ru.vyarus.dropwizard.guice.test.log.RecordedLogs#clear()}. + * + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface RecordLogs { + + /** + * Classes to track loggers for. All log events would be recorded when empty. + *

        + * For string logger names use {@link #loggers()} (could be used together with class loggers). + * + * @return logger classes to listen for + */ + Class[] value() default {}; + + /** + * Custom logger names, not based on class name. Useful for listening for entire packages. + *

        + * Works with class loggers ({@link #value()}). + * + * @return string logger names to listen for + */ + String[] loggers() default {}; + + /** + * WARNING: if the current logger configuration is above the required threshold, then logger level would be updated! + * For example, if global logger level is set to WARN, but recorder level set to DEBUG then logger level would + * be reduced to receive all required events. + * + * @return required events threshold + */ + Level level() default Level.WARN; + + /** + * By default, recorded event reset after each test method. Use to disable automatic cleanup. Note that + * events could be cleared directly with {@link ru.vyarus.dropwizard.guice.test.log.RecordedLogs#clear()}. + * + * @return true to clean up recorded events after test + */ + boolean autoReset() default true; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockBean.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockBean.java new file mode 100644 index 000000000..0ccb4f494 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockBean.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.mock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Replace any guice service with mockito mock in test (using guice module overrides). + *

        + * Important: requires mockito dependency! + *

        + * Example: {@code @MockBean Service mock}. May be used for static and instance fields. + *

        + * Note: could be used for spy objects creation: {@code @MockBean Service spy = Mockito.spy(instance)}. This might + * be useful in cases when service bound by instance and automatic spy (@SpyBean) can't be used. + *

        + * Mock field might be initialized manually: {@code @MockBean Service mock = Mockito.mock(Service.class)}. + * Manual mocks in instance field must be synchronized with the correct guicey extension declaration (by default, + * injector created per test class and test instance created per method, so it is impossible to "see" mocks declared + * in instance fields). Incorrect usage would be immediately reported with error. + *

        + * Mock stubs could be configured in test beforeEach method: {@code Mockito.when(mock).something().thenReturn("ok")}. + *

        + * Note that you can use {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean} with manual mock + * initialization with the almost same result, except it would not be cleared before each test. + *

        + * Mocks reset called after each test method. Could be disabled with {@link #autoReset()} + *

        + * Could also be used for spy objects registration of beans bound by instance (!) + * (so {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean} could not be used): + * {@code @MockBean Service spy = Mockito.spy(new Service())}. + * Spy should also be used when mock must be created from an abstract class (preserving abstract methods): + * {@code @MockBean AbstractService mock = Mockito.spy(AbstractService.class)}. + *

        + * Mockito provide the detailed report of used mock methods and redundant stub definitions. Use {@link #printSummary()} + * to enable this report (printed after each test method). + *

        + * Guicey extension debug ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp#debug()}) enables + * mock fields debug: all recognized annotated fields would be printed to console. + *

        + * Limitation: any aop, applied to the original bean, will not work with mock (because guice can't apply aop to + * instances)! Use {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean} instead if aop is important. + * Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface MockBean { + + /** + * Note: mock could be reset manually with {@link org.mockito.Mockito#reset(Object[])}. + * + * @return true to reset mock after each test method + */ + boolean autoReset() default true; + + /** + * Native mockito mock usage report: shows called methods and stubbed, but not used methods. + * + * @return true to print mock summary after each test + */ + boolean printSummary() default false; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockFieldsSupport.java new file mode 100644 index 000000000..3cd727e17 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/mock/MockFieldsSupport.java @@ -0,0 +1,118 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.mock; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.Mockito; +import org.mockito.internal.util.MockUtil; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.mock.MocksHook; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean} test fields support implementation. + *

        + * Annotated fields resolved in time of guicey extension initialization (beforeAll or beforeEach). + * Register override bindings for provided mock instances (manual or created automatically). + * See debug report to be sure what value was actually used: manual or automatic (field might be assigned too late). + *

        + * In beforeAll injects static values, in beforeEach inject both (in case if beforeAll wasn't called). + * Calls mocks reset after each test. + * + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +public class MockFieldsSupport extends AnnotatedTestFieldSetup { + + private static final String TEST_MOCK_FIELDS = "TEST_MOCK_FIELDS"; + private static final String FIELD_MOCK = "FIELD_MOCK"; + + private final MocksHook hook = new MocksHook(); + + /** + * Create support. + */ + public MockFieldsSupport() { + super(MockBean.class, Object.class, TEST_MOCK_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + // nothing + } + + @Override + protected void registerHooks(final TestExtension extension) { + extension.hooks(hook); + } + + @Override + @SuppressWarnings("unchecked") + protected void initializeField(final AnnotatedField field, final Object userValue) { + final Class type = field.getType(); + if (userValue != null) { + Preconditions.checkState(MockUtil.isMock(userValue), getDeclarationErrorPrefix(field) + + "initialized instance is not a mockito mock object. Either provide correct mock or remove value " + + "and let extension create mock automatically."); + hook.mock(type, (K) userValue); + } else { + // no need to store custom data for manual value - injectFieldValue not called for manual values + field.setCustomData(FIELD_MOCK, hook.mock(type)); + } + } + + @Override + protected void beforeValueInjection(final EventContext context, final AnnotatedField field) { + // nothing: no existing binding validation because jit injections might be used + } + + @Override + protected Object injectFieldValue(final EventContext context, final AnnotatedField field) { + return Preconditions.checkNotNull(field.getCustomData(FIELD_MOCK), "Mock not created"); + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void report(final EventContext context, + final List> annotatedFields) { + final StringBuilder report = new StringBuilder("\nApplied mocks (@") + .append(MockBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); + fields.forEach(field -> report.append( + String.format("\t%-30s %-20s %s%n", + '#' + field.getField().getName(), + RenderUtils.renderClassLine(field.getType()), + field.isCustomDataSet(FIELD_MANUAL) ? "MANUAL" : "AUTO"))); + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, final Object value) { + // only after test (mock might be used in setup) + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void afterTest(final EventContext context, + final AnnotatedField field, final Object value) { + if (field.getAnnotation().printSummary()) { + final String res = Mockito.mockingDetails(value).printInvocations(); + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + + "@" + MockBean.class.getSimpleName() + " stats on [After each] for " + + TestSetupUtils.getContextTestName(context.getJunitContext()) + ":\n\n" + + Arrays.stream(res.split("\n")).map(s -> "\t" + s).collect(Collectors.joining("\n"))); + } + if (field.getAnnotation().autoReset()) { + Mockito.reset(value); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/RestStubFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/RestStubFieldsSupport.java new file mode 100644 index 000000000..430569c11 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/RestStubFieldsSupport.java @@ -0,0 +1,188 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.rest; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.rest.RestStubsHook; +import ru.vyarus.dropwizard.guice.test.rest.StubRestConfig; +import ru.vyarus.dropwizard.guice.test.rest.support.ExtensionsSelector; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest} field support implementation. + *

        + * Only one annotated field is supported. Ready to use rest client injected as field value. + *

        + * By default, all rest resources and jersey extensions registered in application are started. Dropwizard default + * extensions are also registered - so "stub" completely reproduce application state. + *

        + * As rest stub ignores web extensions (servlets, filters) then guicey disables all web extensions to avoid confusion + * (by logged installed extensions). + *

        + * Jersey container is started just after guicey bundle processing (and not in junit beforeAll) to support + * guicey jersey report, which is reported just after application initialization (before beforeAll call). + *

        + * AnnotatedTestFieldSetup used because of implemented Nested classes workflow (so nested class could see + * a rest client declared in root). + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class RestStubFieldsSupport extends AnnotatedTestFieldSetup + implements GuiceyConfigurationHook { + + private static final String TEST_RESOURCES_FIELD = "TEST_RESOURCES"; + private static final String STUB_REST_CLIENT_KEY = "STUB_REST_CLIENT_KEY"; + private RestStubsHook restStubs; + + /** + * Create support. + */ + public RestStubFieldsSupport() { + super(StubRest.class, RestClient.class, TEST_RESOURCES_FIELD); + } + + /** + * Static lookup for registered stubs rest client. Might be used by other extensions to get access to + * rest client. + *

        + * Note: stubs rest client is registered AFTER application startup. + * + * @param context junit context + * @return rest client or null if not registered or requested too early + */ + public static Optional lookupRestClient(final ExtensionContext context) { + return Optional.ofNullable((RestClient) context + .getStore(ExtensionContext.Namespace.create(RestStubFieldsSupport.class)) + .get(STUB_REST_CLIENT_KEY)); + } + + @Override + public void configure(final GuiceBundle.Builder builder) throws Exception { + builder.onGuiceyStartup((config, env, injector) -> + Preconditions.checkState(!new EventContext(setupContext, false).isWebStarted(), + "Resources stubbing is useless when application is fully started. Use it with @" + + TestGuiceyApp.class.getSimpleName() + " where web services not started in " + + "order to start lightweight container with rest services.")); + } + + @Override + protected void registerHooks(final TestExtension extension) { + Preconditions.checkState(fields.size() == 1, "Multiple @" + StubRest.class.getSimpleName() + + " fields declared. To avoid confusion with the configuration, only one field is supported."); + + restStubs = new RestStubsHook(getConfig(fields.get(0).getAnnotation())); + + extension.hooks(restStubs, this); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + // not used + } + + @Override + protected void initializeField(final AnnotatedField field, final RestClient userValue) { + // not used + } + + @Override + protected void beforeValueInjection(final EventContext context, + final AnnotatedField field) { + context.getJunitContext().getStore(ExtensionContext.Namespace.create(RestStubFieldsSupport.class)) + .put(STUB_REST_CLIENT_KEY, restStubs.getRestClient()); + } + + @Override + protected RestClient injectFieldValue(final EventContext context, + final AnnotatedField field) { + // inject rest client as value + return Preconditions.checkNotNull(restStubs.getRestClient(), "Rest stub is required"); + } + + @Override + @SuppressWarnings({"PMD.SystemPrintln", "MultipleStringLiterals", "PMD.ConsecutiveLiteralAppends"}) + protected void report(final EventContext context, + final List> annotatedFields) { + + final StringBuilder report = new StringBuilder(500); + report.append("REST stub (@").append(StubRest.class.getSimpleName()) + .append(") started on ").append(setupContextName).append(":\n") + + .append("\n\tJersey test container factory: ") + .append(restStubs.getJerseyStub().getTestContainerFactory().getClass().getName()) + .append("\n\tDropwizard exception mappers: ") + .append(annotatedFields.get(0).getAnnotation().disableDropwizardExceptionMappers() + ? "DISABLED" : "ENABLED").append('\n'); + + final ExtensionsSelector selector = new ExtensionsSelector(context.getBean(GuiceyConfigurationInfo.class)); + final List> resources = selector.getResources(); + + report.append("\n\t").append(resources.size()).append(" resources"); + final int disabledResources = selector.getDisabledResourcesCount(); + if (disabledResources > 0) { + report.append(" (disabled ").append(disabledResources).append(')'); + } + report.append(":\n"); + resources.forEach(resource -> report.append( + String.format("\t\t%s%n", RenderUtils.renderClassLine(resource)))); + + + final List> extensions = selector.getExtensions(); + report.append("\n\t").append(extensions.size()).append(" jersey extensions"); + final int disabledExtensions = selector.getDisabledExtensionsCount(); + if (disabledExtensions > 0) { + report.append(" (disabled ").append(disabledExtensions).append(')'); + } + report.append(":\n"); + + extensions.forEach(resource -> report.append( + String.format("\t\t%s%n", RenderUtils.renderClassLine(resource)))); + report.append("\n\tUse .printJerseyConfig() report to see ALL registered jersey extensions " + + "(including dropwizard)\n"); + + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, final RestClient value) { + // not used + } + + @Override + protected void afterTest(final EventContext context, + final AnnotatedField field, final RestClient value) { + // reset client defaults + if (field.getAnnotation().autoReset()) { + value.reset(); + } + } + + private StubRestConfig getConfig(final StubRest annotation) { + final StubRestConfig config = new StubRestConfig(); + Collections.addAll(config.getResources(), annotation.value()); + Collections.addAll(config.getDisableResources(), annotation.disableResources()); + Collections.addAll(config.getJerseyExtensions(), annotation.jerseyExtensions()); + config.setDisableAllJerseyExtensions(annotation.disableAllJerseyExtensions()); + config.setDisableDropwizardExceptionMappers(annotation.disableDropwizardExceptionMappers()); + Collections.addAll(config.getDisableJerseyExtensions(), annotation.disableJerseyExtensions()); + config.setLogRequests(annotation.logRequests()); + config.setContainer(annotation.container()); + return config; + } + +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/StubRest.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/StubRest.java new file mode 100644 index 000000000..07370b873 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/rest/StubRest.java @@ -0,0 +1,155 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.rest; + +import ru.vyarus.dropwizard.guice.test.rest.TestContainerPolicy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Resources stubbing: start lightweight rest container (with, probably, only one or a couple of services + * to test) without web (no servlets, filters, etc. would work). This is the same as dropwizard's + * {@link io.dropwizard.testing.junit5.ResourceExtension}, but with full guice support. Rest extensions like exception + * mappers, filters, etc. could also be disabled (including dropwizard default extensions). As guicey knows all + * registered extensions, it provides them automatically (so, by default, no configuration is required - all jersey + * resources and extensions are available). + *

        + * It is not quite correct to call it stubs - because this is a fully functional rest (same as in normal application). + * It is called stub just to highlight customization ability (for example, we can start only resource with just a + * bunch of enabled extensions). + * Other stubbing extensions should simplify testing resources (e.g., by mocking authorization support, etc.): + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean}. + *

        + * Could be used ONLY with {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp} (or + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension}). Stubbing is enabled by a field + * declaration: {@code @StubRest RestClient rest}. This declaration activates custom rest container, started + * on random port and with all rest resources and extensions. As test container does not support web resources (will + * simply not work), all registered web extensions are disabled (to avoid confusion by console output), together with + * {@link com.google.inject.servlet.GuiceFilter}. + *

        + * Only one rest stub field could be declared in test! Rest client is injected into the declared field: use it to call + * rest methods: {@code Something result = rest.get("/relative/rest/path", Something.class)} (see + * {@link ru.vyarus.dropwizard.guice.test.rest.RestClient} class for usage info). + *

        + * To limit started rest resources, simply specify what resources to start (test could start only one resource to + * test it): {@code @StubRest(Resources1.class, Resource2.class)}. Alternatively, if many resources required, + * you can disable some resources: {@code @StubRest(disableResources = {Resources1.class, Resource2.class})}. + *

        + * By default, all jersey extensions, declared in application are applied. You can disable all of them: + * {@code @StubRest(disableAllJerseyExtensions = true)} (note that dropwizard extensions remain!). + * Or you can specify just required extensions: {@code @StubRest(jerseyExtensions = {Ext1.class, Ext2.class})}. + * Also, only some extensions could be disabled: {@code @StubRest(disableJerseyExtensions = {Ext1.class, Ext2.class})}. + *

        + * Default dropwizard's exception mappers could be disabled with: + * {@code @StubRest(disableDropwizardExceptionMappers = true)}. This is very useful for testing rest errors (to + * receive exception instead of generic 500 response). + *

        + * By default, in-memory container (lightweight, but not all features supported) would be used and grizzly container, + * if available in classpath. Use {@link #container()} option to force the exact container type (prevent incorrect + * usage). + *

        + * Use {@code @TestGuiceyApp(debug = true)} to see a list of active rest resources and jersey extensions. + * The full list of enabled jersey extensions (including dropwizard and jersey core) could be seen with + * {@code .printJerseyConfig()} option, activated in application (guice builder) or using a hook. + *

        + * Log requests option ({@code @StubRest(logRequests = true)} activates complete requests and responses logging. + *

        + * Warn: the default guicey client ({@link ru.vyarus.dropwizard.guice.test.ClientSupport}) would not work - but you + * don't need it as a complete rest client provided. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface StubRest { + + /** + * By default, all resources would be available. Use this option to run a subset of resources. + * + * @return resources to use in stub + * @see #disableResources() to disable some default resources + */ + Class[] value() default {}; + + /** + * NOTE: if resources specified in {@link #value()} then the disable option would be ignored (all required + * resources already specified). This option is useful to exclude only some resources from the registered + * application resources + *

        + * Important: affects only resources, recognized as guicey extensions. Manually registered resources + * would remain! + * + * @return resources to disable + */ + Class[] disableResources() default {}; + + /** + * By default, all jersey extension, registered in application, would be registered. Use this option to specify + * exact required extensions (all other application extensions would be disabled). + *

        + * Important: this affects only guicey extensions (all other guicey extension would be simply disabled). + * To disable core dropwizard exception mappers use {@link #disableDropwizardExceptionMappers()}. + * + * @return jersey extensions to use in stub + */ + Class[] jerseyExtensions() default {}; + + /** + * NOTE: if extensions specified in {@link #jerseyExtensions()} then the disable option would be ignored (all + * required extensions already specified). + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #disableDropwizardExceptionMappers()}. + * + * @return true to disable all application jersey extensions + */ + boolean disableAllJerseyExtensions() default false; + + /** + * By default, all dropwizard exception mappers registered (same as in real application). For tests, it might be + * more convenient to disable them and receive direct exception objects after test. + * + * @return true dropwizard exception mappers + */ + boolean disableDropwizardExceptionMappers() default false; + + /** + * NOTE: if extensions specified in {@link #jerseyExtensions()} then the disable option would be ignored (all + * required extensions already specified). This option is useful to exclude only some extensions from the registered + * application jersey extensions. + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #disableDropwizardExceptionMappers()}. + * + * @return jersey extensions to disable + */ + Class[] disableJerseyExtensions() default {}; + + /** + * By default, the rest client state is re-set after each test. Client could be reset with manual + * {@link ru.vyarus.dropwizard.guice.test.rest.RestClient#reset()} call. + * + * @return false to disable automatic rest client state reset + */ + boolean autoReset() default true; + + /** + * Requests log enabled by default (like in {@link ru.vyarus.dropwizard.guice.test.ClientSupport}). + * + * @return true to print all requests and responses into console + */ + boolean logRequests() default true; + + /** + * By default, use a lightweight in-memory container, but switch to grizzly when it's available in classpath + * (this is the default behavior of {@link org.glassfish.jersey.test.JerseyTest}). + * + * @return required test container policy + */ + TestContainerPolicy container() default TestContainerPolicy.DEFAULT; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyBean.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyBean.java new file mode 100644 index 000000000..609b41577 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyBean.java @@ -0,0 +1,79 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.spy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Consumer; + +/** + * Replace any guice service with mockito spy in test. The difference with mock: spy wraps around the real service(!) + * and could be used to validate called service methods (verify incoming parameters and output value). + *

        + * Important: requires mockito dependency! + *

        + * In contrast to mocks and stubs, spies work with guice AOP: all calls to service are intercepted and + * passed through the spy object (as "proxy"). That also means that all aop, applied to the original bean, would + * work (in contrast to mocks). + *

        + * As spy requires real bean instance - spy object is created just after injector creation (and AOP interceptor + * redirects into it (then real bean called)). Spy object, injected to a field would not be the same instance as + * injected bean ({@code @Inject SpiedService}) because injected bean would be a proxy, handling guice AOP. + *

        + * Calling bean methods directly on spy is completely normal (guice bean just redirects calls to spy object)! + *

        + * Example: {@code @SpyBean Service spy}. May be used for static and instance fields. + *

        + * Spy field CAN'T be initialized manually. Use {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean} + * or {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean} + * instead for manual spy objects initialization. Such manual initialization could be required for spies created + * from abstract classes (or multiple interfaces): in this case actual bean instance is not required, and so mocks + * support could be used instead: {@code @MockBean AbstractService spy = Mockito.spy(AbstractService.class)}. + *

        + * Spy stubs could be configured in test beforeEach method: {@code Mockito.roReturn("ok").when(spy).something()}. + *

        + * Spies reset called before and after each test method. Could be disabled with {@link #autoReset()} + *

        + * Mockito provide the detailed report of used mock methods and redundant stub definitions. Use {@link #printSummary()} + * to enable this report (printed after each test method). + *

        + * Guicey extension debug ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp#debug()}) enables + * spy fields debug: all recognized annotated fields would be printed to console. + *

        + * If spies assumed to be used only to validate bean in/out, then + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean} might be used instead: it is simply collects + * called methods with argument and results, plug measure performance (spies and trackers could be used together). + * + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SpyBean { + + /** + * Note: spy could be reset manually with {@link org.mockito.Mockito#reset(Object[])}. + * + * @return true to reset spy after each test method + */ + boolean autoReset() default true; + + /** + * Native mockito spy usage report: shows called methods and stubbed, but not used methods. + * + * @return true to print spy summary after each test + */ + boolean printSummary() default false; + + /** + * In most cases, spy object could be configured after application startup (using test setUp method). But + * if target spy is used during application startup, this initializer could be used to configure spy object on + * first access and so application startup logic (like managed objects) would use already configured spy during + * startup. + *

        + * Consumer accepts already created spy instance. + * + * @return optional spy object initializer + */ + Class>[] initializers() default {}; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyFieldsSupport.java new file mode 100644 index 000000000..6ceb5a2ec --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/spy/SpyFieldsSupport.java @@ -0,0 +1,127 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.spy; + +import com.google.common.base.Preconditions; +import com.google.inject.Binding; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; +import ru.vyarus.dropwizard.guice.test.spy.SpiesHook; +import ru.vyarus.dropwizard.guice.test.spy.SpyProxy; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean} test fields support implementation. + *

        + * Annotated fields resolved in time of guicey extension initialization (beforeAll or beforeEach). + * Register aop interceptor around target service to intercept all calls, and redirect all calls through spy object. + * This way, real bean becomes spied and still injected everywhere. + *

        + * Manual values are not supported: @MockBean should be used instead. + *

        + * In beforeAll injects static values, in beforeEach inject both (in case if beforeAll wasn't called). + * Calls spies reset after each test. + * + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +public class SpyFieldsSupport extends AnnotatedTestFieldSetup { + + private static final String TEST_SPY_FIELDS = "TEST_SPY_FIELDS"; + private static final String FIELD_SPY = "FIELD_SPY"; + + private final SpiesHook hook = new SpiesHook(); + + /** + * Create support. + */ + public SpyFieldsSupport() { + super(SpyBean.class, Object.class, TEST_SPY_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, final AnnotatedField field) { + // nothing + } + + @Override + protected void registerHooks(final TestExtension extension) { + extension.hooks(hook); + } + + @Override + @SuppressWarnings("unchecked") + protected void initializeField(final AnnotatedField field, final Object userValue) { + if (userValue != null) { + throw new IllegalStateException(getDeclarationErrorPrefix(field) + + "manual spy declaration is not supported. " + + "Use @" + MockBean.class.getSimpleName() + " instead to specify manual spy object."); + } + final SpyProxy proxy = hook.spy((Class) field.getType()); + final Class>[] initializers = (Class>[]) (Class[]) field.getAnnotation().initializers(); + for (Class> initializer : initializers) { + proxy.withInitializer(InstanceUtils.create(initializer)); + } + field.setCustomData(FIELD_SPY, proxy); + } + + @Override + protected void beforeValueInjection(final EventContext context, final AnnotatedField field) { + final SpyProxy spy = Preconditions.checkNotNull(field.getCustomData(FIELD_SPY)); + final Binding binding = context.getInjector().getBinding(spy.getType()); + Preconditions.checkState(!isInstanceBinding(binding), getDeclarationErrorPrefix(field) + + "target bean '%s' bound by instance and so can't be spied", spy.getType().getSimpleName()); + } + + @Override + protected Object injectFieldValue(final EventContext context, final AnnotatedField field) { + // inject already initialized spy from aop interceptor + final SpyProxy aop = field.getCustomData(FIELD_SPY); + return aop.getSpy(); + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void report(final EventContext context, final List> annotatedFields) { + final StringBuilder report = new StringBuilder("\nApplied spies (@") + .append(SpyBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); + fields.forEach(field -> report.append( + String.format("\t%-30s %-20s%n", + '#' + field.getField().getName(), + RenderUtils.renderClassLine(field.getType())))); + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, final Object value) { + // only after test (spy might be used in setup) + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void afterTest(final EventContext context, + final AnnotatedField field, final Object value) { + if (field.getAnnotation().printSummary()) { + final String res = Mockito.mockingDetails(value).printInvocations(); + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + + "@" + SpyBean.class.getSimpleName() + " stats on [After each] for " + + TestSetupUtils.getContextTestName(context.getJunitContext()) + ":\n\n" + + Arrays.stream(res.split("\n")).map(s -> "\t" + s).collect(Collectors.joining("\n"))); + } + if (field.getAnnotation().autoReset()) { + Mockito.reset(value); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubBean.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubBean.java new file mode 100644 index 000000000..615f7fbba --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubBean.java @@ -0,0 +1,55 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.stub; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Replace any guice service with its stub in test (using guice module overrides). Consider stubs as a hand-made + * mocks. + *

        + * Example: suppose we have some {@code Service} and we need to modify it for tests, so we extend it with + * {@code class ServiceStub extends Service} and override required methods. Register stub in test field + * as {@code @StubBean(Service.class) ServiceStub stub;} (could be a static filed). Internally, overriding guice + * binding would be created: {@code bind(Service.class).to(ServiceStub.class).in(Singleton.class)} so guice would + * create stub instead of the original service. Guice would create stub instance, so injections would work inside it + * (and AOP). + *

        + * If stub field is initialized manually, then manual instance would be injected into guice context. In case when + * guicey extension started per class and non-static stub field is initialized, guicey will throw an error + * (because it is impossible to get non-static field value in time of guice context creation). + * Pay attention that guice AOP will not be applied to the manually created stub! + *

        + * More canonical example with interface: suppose we have {@code bind(IServie.clas).to(ServiceImpl.class))}. In this + * case, stub could simply implement interface, instead of extending class: + * {@code class ServiceStub implements IService}. Stub field must declare interface as a binding key: + * {@code @StubBean(IService.class) ServiceStub stub}; + *

        + * Guicey test extension debug option would also activate printing all detected stub fields. + *

        + * Stub object would not be re-created for each test in case of per-class test (where application created once for + * all test methods). If you need to perform some cleanups between tests, stub class must implement + * {@link ru.vyarus.dropwizard.guice.test.stub.StubLifecycle} and it's before() and after() methods + * would be called before and after each test method. + *

        + * Just in case: guice injection will also return stabbed bean (because stub instance is created by guice or + * instance bound into it). + *

        + * Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface StubBean { + + /** + * The class that this stub must override (could be service itself or base interface). + * Note that stub class can't be the same as overriding class. + * + * @return replaced service class + */ + Class value(); +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubFieldsSupport.java new file mode 100644 index 000000000..d5c98198c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/stub/StubFieldsSupport.java @@ -0,0 +1,113 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.stub; + +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; +import ru.vyarus.dropwizard.guice.test.stub.StubsHook; + +import java.util.List; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean} test fields support implementation. + *

        + * Annotated fields resolved in time of guicey extension initialization (beforeAll or beforeEach). + * Register override bindings for provided stubs (singletons!). Stub instances created by guice (to be able to use + * injections inside it). If stub field is initialized manually - this value would be bound into guice context + * (see debug report to be sure what value was actually used - field might be assigned too late). + *

        + * In beforeAll injects static values, in beforeEach inject both (in case if beforeAll wasn't called). + *

        + * For stub objects, implementing {@link ru.vyarus.dropwizard.guice.test.stub.StubLifecycle} before and + * after methods called on beforeEach and afterEach to perform cleanups. + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public class StubFieldsSupport extends AnnotatedTestFieldSetup { + + // test context storage key for resolved fields + private static final String TEST_STUB_FIELDS = "TEST_STUB_FIELDS"; + private final StubsHook hook = new StubsHook(); + + /** + * Create support. + */ + public StubFieldsSupport() { + super(StubBean.class, Object.class, TEST_STUB_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + final Class key = field.getAnnotation().value(); + final Class type = field.getType(); + if (!key.isAssignableFrom(type)) { + throw new IllegalStateException(getDeclarationErrorPrefix(field) + type.getSimpleName() + + " is not assignable to " + key.getSimpleName()); + } + } + + @Override + protected void registerHooks(final TestExtension extension) { + extension.hooks(hook); + } + + @Override + @SuppressWarnings("unchecked") + protected void initializeField(final AnnotatedField field, final Object userValue) { + final Class key = (Class) field.getAnnotation().value(); + if (userValue != null) { + hook.stub(key, (K) userValue); + } else { + // bind original type to stub - guice will instantiate it + hook.stub(key, (K) field.getType()); + } + } + + @Override + protected void beforeValueInjection(final EventContext context, final AnnotatedField field) { + // nothing + } + + @Override + protected Object injectFieldValue(final EventContext context, final AnnotatedField field) { + // if not declared, stub value created by guice + return context.getBean(field.getAnnotation().value()); + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void report(final EventContext context, + final List> fields) { + final StringBuilder report = new StringBuilder("\nApplied stubs (@") + .append(StubBean.class.getSimpleName()).append(") on ").append(setupContextName).append(":\n\n"); + fields.forEach(field -> report.append( + String.format("\t%-40s %-10s %20s >> %-20s%n", + field.getField().getDeclaringClass().getSimpleName() + "." + field.getField().getName(), + field.isCustomDataSet(FIELD_MANUAL) ? "MANUAL" : "GUICE", + field.getAnnotation().value().getSimpleName(), + field.getType().getSimpleName()))); + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, + final Object value) { + if (value instanceof StubLifecycle) { + ((StubLifecycle) value).before(); + } + } + + @Override + protected void afterTest(final EventContext context, + final AnnotatedField field, + final Object value) { + if (value instanceof StubLifecycle) { + ((StubLifecycle) value).after(); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackBean.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackBean.java new file mode 100644 index 000000000..3ab6ecb4c --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackBean.java @@ -0,0 +1,118 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.track; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.temporal.ChronoUnit; + +/** + * Tracks method calls on any guice bean and records arguments and return values, together with measuring time. + *

        + * Useful for validating called method arguments and return value (when service called indirectly - by another top-level + * service). In this sense it is very close to mockito spy + * ({@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}), but api is simpler. + * Tracker collects both raw in/out objects and string (snapshot) version (because mutable objects could change). + * Raw objects holding could be disabled with {@link #keepRawObjects()}. + *

        + * Another use-case is slow methods detection: tracker counts each method execution time, and after test could + * print a report indicating the slowest methods. Or it could be used to simply print all called methods to console + * with {@link #trace()} (could be useful during behavior investigations). Another option is to configure + * a slow method threshold: then only methods above a threshold would be logged with WARN. + *

        + * Example usage: {@code @TrackBean Tracker tracker}. All calls to {@code Service} bean would be tracked. + * The field might be static. + *

        + * Manual field initialization is not allowed. + *

        + * Tracking is implemented with a custom AOP handler which intercepts all bean calls and record them. + *

        + * Can be used together with mocks, spies or stubs ({@link ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean}, + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean}). + *

        + * Guicey extension debug ({@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp#debug()}) enables + * tracker fields debug: all recognized annotated fields would be printed to console. Also, after each test method + * it would print performance stats for called methods in all registered trackers. + *

        + * Individual tracker report could be enabled with {@link #printSummary()} - will print called methods stats + * for exact tracker (independent of guicey extension debug option). + *

        + * By default, tracker re-set after each test method. Use {@link #autoReset()} to collect tracking data for the entire + * test. + *

        + * Limitation: could track only beans, created by guice (due to used AOP). Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TrackBean { + + /** + * When enabled, prints called method just after it's execution (with called arguments and returned result). + * Not enabled by default to avoid output mess in case when many methods would be called during test. + * + * @return true to print each method execution + */ + boolean trace() default false; + + /** + * Print warnings about methods executing longer than the specified threshold. Set to 0 to disable warnings. + * + * @return slow method threshold (in seconds, by default - see {@link #slowMethodsUnit()}) + */ + long slowMethods() default 5; + + /** + * Unit for {@link #slowMethods()} threshold value (seconds by default). + * + * @return unit for threshold value + */ + ChronoUnit slowMethodsUnit() default ChronoUnit.SECONDS; + + /** + * It is more likely that trackers would be used mostly for "call and verify" scenarios where keeping raw + * arguments makes perfect sense. That's why it's enabled by default. + *

        + * Important: method arguments and the result objects state could be mutable and change after or during method + * execution (and so be irrelevant for tracks analysis). For such cases, the tracker always holds string + * representations of method arguments and the result (rendered in method execution time). + *

        + * It makes sense to disable option if too many method executions appear during the test (e.g., tracker used + * for performance metrics). + * + * @return true to keep raw arguments and result objects + */ + boolean keepRawObjects() default true; + + /** + * Required to keep called method toString rendering readable in case of large strings used. + * Note that for non-string objects, an object type with identity hash would be shown (not rely on toString + * because it would be too much unpredictable). + * + * @return maximum length of string in method parameter or returned result + */ + int maxStringLength() default 30; + + /** + * Note: tracker could be cleared manually with {@link ru.vyarus.dropwizard.guice.test.track.Tracker#clear()}. + * + * @return true to reset tracker (remove collected stats) after each test method + */ + boolean autoReset() default true; + + /** + * Note that the summary for all registered trackers is printed when the guicey extension debug option enabled. + * This option exists to be able to print summary for a single tracker, independent of the debug option. + *

        + * The report would be shown after each test method. + *

        + * Note that such reports could be built at any time manually with {@code tracker.getStats().render()} + * (or any custom report using {@code tracker.getStats().getMethods()}). + * + * @return true to print summary after test + */ + boolean printSummary() default false; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackerFieldsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackerFieldsSupport.java new file mode 100644 index 000000000..693ae6f3f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/track/TrackerFieldsSupport.java @@ -0,0 +1,147 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.ext.track; + +import com.google.common.base.Preconditions; +import com.google.inject.Binding; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedTestFieldSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.track.Tracker; +import ru.vyarus.dropwizard.guice.test.track.TrackersHook; +import ru.vyarus.dropwizard.guice.test.track.stat.TrackerStats; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; +import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; + +import java.util.List; + +/** + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean} test fields support implementation. + *

        + * Annotated fields resolved in time of guicey extension initialization (beforeAll or beforeEach). + * Register aop interceptor around target service to intercept all calls. + *

        + * In beforeAll injects static values, in beforeEach inject both (in case if beforeAll wasn't called). + * Calls tracker reset after each test. + * + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +public class TrackerFieldsSupport extends AnnotatedTestFieldSetup { + // test context storage key for resolved fields + private static final String TEST_TRACKER_FIELDS = "TEST_TRACKER_FIELDS"; + private static final String FIELD_TRACKER = "FIELD_TRACKER"; + private static final String DOUBLE_NL = ":\n\n"; + + private final TrackersHook hook = new TrackersHook(); + + /** + * Create support. + */ + public TrackerFieldsSupport() { + super(TrackBean.class, Tracker.class, TEST_TRACKER_FIELDS); + } + + @Override + protected void fieldDetected(final ExtensionContext context, + final AnnotatedField field) { + final Class type = field.getTypeParameters().get(0); + if (type == Object.class) { + throw new IllegalStateException(getDeclarationErrorPrefix(field) + "tracked service must be declared as " + + "a tracker object generic: Tracker"); + } + } + + @Override + protected void registerHooks(final TestExtension extension) { + extension.hooks(hook); + } + + @Override + protected void initializeField(final AnnotatedField field, final Tracker userValue) { + Preconditions.checkState(userValue == null, getDeclarationErrorPrefix(field) + + "tracker instance can't be provided manually"); + final Class type = field.getTypeParameters().get(0); + final TrackBean ann = field.getAnnotation(); + final Tracker tracker = hook.track(type) + .trace(ann.trace()) + .slowMethods(ann.slowMethods(), ann.slowMethodsUnit()) + .keepRawObjects(ann.keepRawObjects()) + .maxStringLength(ann.maxStringLength()) + .add(); + field.setCustomData(FIELD_TRACKER, tracker); + + } + + @Override + protected void beforeValueInjection(final EventContext context, final AnnotatedField field) { + final Tracker tracker = Preconditions.checkNotNull(field.getCustomData(FIELD_TRACKER)); + final Binding binding = context.getInjector().getBinding(tracker.getType()); + Preconditions.checkState(!isInstanceBinding(binding), getDeclarationErrorPrefix(field) + + "target bean '%s' bound by instance and so can't be tracked", tracker.getType().getSimpleName()); + } + + @Override + protected Tracker injectFieldValue(final EventContext context, final AnnotatedField field) { + return field.getCustomData(FIELD_TRACKER); + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void report(final EventContext context, + final List> annotatedFields) { + final StringBuilder report = new StringBuilder("\nApplied trackers (@") + .append(TrackBean.class.getSimpleName()).append(") on ").append(setupContextName).append(DOUBLE_NL); + fields.forEach(field -> report.append( + String.format("\t%-30s %-20s%n", + '#' + field.getField().getName(), + RenderUtils.renderClassLine(field.getCustomData(FIELD_TRACKER).getType())))); + System.out.println(report); + } + + @Override + protected void beforeTest(final EventContext context, + final AnnotatedField field, final Tracker value) { + // no clean to keep tracks from setup stage + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + protected void afterTest(final EventContext context, + final AnnotatedField field, final Tracker value) { + final TrackBean ann = field.getAnnotation(); + if (ann.printSummary()) { + final Tracker tracker = field.getCustomData(FIELD_TRACKER); + // report for exact tracker (activated with the annotation option - works without debug enabling) + if (!tracker.isEmpty()) { + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + + "Tracker<" + tracker.getType().getSimpleName() + ">" + " stats (sorted by median) for " + + TestSetupUtils.getContextTestName(context.getJunitContext()) + DOUBLE_NL + + tracker.getStats().render()); + } + } + if (ann.autoReset()) { + value.clear(); + } + } + + @Override + @SuppressWarnings("PMD.SystemPrintln") + public void afterEach(final EventContext context) { + if (context.isDebug()) { + final Tracker[] trackers = fields.stream() + .map(field -> (Tracker) field.getCustomData(FIELD_TRACKER)) + .toArray(Tracker[]::new); + if (trackers.length > 0) { + // report all trackers - works only with debug + System.out.println(PrintUtils.getPerformanceReportSeparator(context.getJunitContext()) + + "Trackers stats (sorted by median) for " + + TestSetupUtils.getContextTestName(context.getJunitContext()) + DOUBLE_NL + + new TrackerStats(trackers).render()); + } + } + // cleanup all tracks + super.afterEach(context); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/param/Jit.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/param/Jit.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/param/Jit.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/param/Jit.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/LogsSelector.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/LogsSelector.java new file mode 100644 index 000000000..8f4c19aad --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/LogsSelector.java @@ -0,0 +1,168 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Log records selector object. To avoid tons of selection methods with different parameters, all selection methods + * return sub-selector object for further selections. At any selection step events could be obtained with + * {@link #events()} or {@link #messages()}. + * + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class LogsSelector { + + /** + * Events. + */ + protected final List list; + + /** + * Create collector. + * + * @param list events list + */ + public LogsSelector(final List list) { + this.list = list; + } + + /** + * @return true when no logs recorded + */ + public boolean empty() { + return count() == 0; + } + + /** + * @return count of log records + */ + public int count() { + return list.size(); + } + + /** + * @return list of raw events + */ + public List events() { + return list; + } + + /** + * @return formatted messages (without logger class, raw message) + */ + public List messages() { + return messages(ILoggingEvent::getFormattedMessage); + } + + /** + * @param mapper mapper function + * @return messages after custom formatter + */ + public List messages(final Function mapper) { + return list.stream().map(mapper).collect(Collectors.toList()); + } + + /** + * @return the last recorded event + */ + public ILoggingEvent lastEvent() { + return list.isEmpty() ? null : list.get(list.size() - 1); + } + + /** + * @return the last recorded message + */ + public String lastMessage() { + final ILoggingEvent lastEvent = lastEvent(); + return lastEvent == null ? null : lastEvent.getFormattedMessage(); + } + + /** + * @param loggerName logger name + * @return true if logged messages found for required logger + */ + public boolean has(final String loggerName) { + return list.stream().anyMatch(event -> event.getLoggerName().equals(loggerName)); + } + + /** + * @param logger logger class + * @return true if logged messages found for required logger + */ + public boolean has(final Class logger) { + return has(logger.getName()); + } + + /** + * @param level required level + * @return true if logged messages found for required level + */ + public boolean has(final org.slf4j.event.Level level) { + // toString not error - it returns string level representation + final Level reqLevel = Level.valueOf(level.toString()); + return list.stream().anyMatch(event -> event.getLevel().equals(reqLevel)); + } + + /** + * Generic event selector. + * + * @param predicate selection predicate + * @return selector for selected events + */ + public LogsSelector select(final Predicate predicate) { + return new LogsSelector(list.stream().filter(predicate).collect(Collectors.toList())); + } + + /** + * @param levels required levels + * @return sub selector with filtered logs from other levels + */ + public LogsSelector level(final org.slf4j.event.Level... levels) { + // toString not error - it returns string level representation + final List reqLevel = Arrays.stream(levels) + .map(it -> Level.valueOf(it.toString())).collect(Collectors.toList()); + return select(event -> reqLevel.contains(event.getLevel())); + } + + /** + * @param loggerNames logger name + * @return sub selector with filtered logs from other loggers + */ + public LogsSelector logger(final String... loggerNames) { + final List loggers = Arrays.stream(loggerNames).collect(Collectors.toList()); + return select(event -> loggers.contains(event.getLoggerName())); + } + + /** + * @param logger logger class + * @return sub selector with filtered logs from other loggers + */ + public LogsSelector logger(final Class... logger) { + return logger(Arrays.stream(logger).map(Class::getName).toArray(String[]::new)); + } + + /** + * @param messagePart message to find in logged messages + * @return sub selector with log records containing provided string + */ + public LogsSelector containing(final String messagePart) { + return select(event -> event.getFormattedMessage().contains(messagePart)); + } + + /** + * @param regex regular expression + * @return sub selector with log records matching provided regex (matched by find) + */ + public LogsSelector matching(final String regex) { + final Pattern pattern = Pattern.compile(regex); + return select(event -> pattern.matcher(event.getFormattedMessage()).find()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordLogsHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordLogsHook.java new file mode 100644 index 000000000..50dd2019d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordLogsHook.java @@ -0,0 +1,196 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Environment; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.BeforeInitEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Record log events for verification. + * IMPORTANT: works ONLY with logback (would be useless with another logger). + *

        + * Without additional configuration would record all events (from the root logger): + * {@code hook.record().start(Level.INFO)} + * In most cases, it would be more convenient to listen to the exact logger logs: + * {@code hook.record(Service.class).start(Level.INFO)} - listen Service logs (assuming logger created as + * {@code LoggerFactory.getLogger(Service.class)}). + * Entire packages could be listened with: {@code hook.register("com.package").start(Level.INFO)}. + * (class and string loggers could be specified together). + *

        + * Could be used for a quick logger configuration changes in tests (easy switch to TRACE, for example). + *

        + * Recorded events could be inspected with {@link RecordedLogs} object: + * {@code RecordedLogs logs = hook.record().start(Level.INFO);}. + * Raw recorded event objects could be used ({@code logs.getEvents()}) or just string messages + * ({@code logs.getMessages()}). There are many other methods to filter recorded logs. + *

        + * Events recorded for the entire application startup. Dropwizard resets loggers two times: in application constructor + * and just before the run phase (log configuration factory init), so logs listener appender have to be re-registered. + * LIMITATION: would not see run phase logs of dropwizard bundles, registered BEFORE + * {@link ru.vyarus.dropwizard.guice.GuiceBundle} (no way re-attach listener before it). For dropwizard bundles, + * registered after guice bundle (or inside it) - all logs would be visible. + *

        + * Recorded logs could be cleared either with {@link RecordedLogs#clear()} or with {@link #clearLogs()} for all + * registered recorders. + * + * @author Vyacheslav Rusakov + * @since 30.04.2025 + */ +@SuppressWarnings("IllegalIdentifierName") +public class RecordLogsHook implements GuiceyConfigurationHook { + + private final List recorders = new ArrayList<>(); + private final AtomicInteger counter = new AtomicInteger(1); + + @Override + @SuppressWarnings("unchecked") + public void configure(final GuiceBundle.Builder builder) throws Exception { + + // The first re-attach called in time of hooks processing (this happens in time of GuiceBundle builder + // finalization). This is the earliest point after application creation (logs reset in application + // constructor - Application.bootstrapLogging) + recorders.forEach(Recorder::attach); + + builder.listen(event -> { + // Dropwizard resets loggers just before the run phase + // (see logging configuration see io.dropwizard.logging.common.DefaultLoggingFactory). + // All logs in guice bundles, registered before guice bundle would remain invisible, + // but we can register dropwizard bundle here (directly!), which would be registered before all + // dropwizard bundles registered through guicey, and so be able to intercept all logs for such bundles. + // (dropwizard bundles, registered by guicey are always go before guice bundle itself because + // dropwizard calls initialization BEFORE adding bundle - no way to register them after) + if (event.getType().equals(GuiceyLifecycle.BeforeInit)) { + ((BeforeInitEvent) event).getBootstrap().addBundle(new RecordedLogsTrackingBundle()); + } + }); + } + + /** + * Start recorder configuration. If no loggers provided then root logger would be listened (all events). + *

        + * Minimal usage: {@code record().loggers(Service.class).start(Level.INFO)}. + * Could be mixed with string-based loggers declaration: + * {@code record().loggers(Service.class).loggers("some.string.logger").start(Level.INFO)}. + * + * @return builder for additional configuration + */ + public Builder record() { + return new Builder(); + } + + /** + * Clear recorded logs for all registered recorders. + */ + public void clearLogs() { + recorders.forEach(Recorder::clear); + } + + /** + * Detach all registered appenders from logback loggers. + *

        + * Not required as dropwizard reset all logging during application startup + * and so stale appenders would be removed in any case before each new test. + */ + public void destroy() { + recorders.forEach(Recorder::destroy); + } + + /** + * Log recorder configuration builder. + */ + public class Builder { + private String name; + private final List> typedLoggers = new ArrayList<>(); + private final List stringLoggers = new ArrayList<>(); + + /** + * Custom name for logback appender. This might be used to better identify target appender (quite rarely + * required). + * + * @param name custom appender name, used for this recorder + * @return builder instance + */ + public Builder recorderName(final String name) { + this.name = name; + return this; + } + + /** + * Class loggers to listen. + * + * @param typedLoggers loggers + * @return builder instance + */ + public Builder loggers(final Class... typedLoggers) { + Collections.addAll(this.typedLoggers, typedLoggers); + return this; + } + + /** + * Custom logger names, not based on class name. Useful for listening for entire packages. + * + * @param loggers string logger names to listen for + * @return builder instance + */ + public Builder loggers(final String... loggers) { + Collections.addAll(this.stringLoggers, loggers); + return this; + } + + /** + * WARNING: if the current logger configuration is above the required threshold, then logger level would be + * updated! For example, if global logger level is set to WARN, but recorder level set to DEBUG then logger + * level would be reduced to receive all required events. + *

        + * Note: returning logs access object instead of recorder itself to simplify usage: in most cases, + * no additional attach/detach is required - only actual recorded logs access. Recorder object could + * be easily obtained with {@link RecordedLogs#getRecorder()}. + *

        + * Also, note that logs recording starts just after this method call: registration of parent hook is required + * to properly re-bind recorders after dropwizard logs resets (dropwizard resets logging during startup). + * + * @param level required logging level + * @return recorded logs access object + */ + public RecordedLogs start(final Level level) { + final List loggers = new ArrayList<>(); + if (!typedLoggers.isEmpty()) { + loggers.addAll(typedLoggers.stream().map(Class::getName).toList()); + } + loggers.addAll(stringLoggers); + String id = name; + if (id == null) { + id = "Logs recorder #" + counter.getAndIncrement(); + } + final Recorder recorder = new Recorder(id, level == null ? Level.WARN : level, loggers); + recorders.add(recorder); + + // attach here (before application run) to gather all possible logs, but dropwizard will reset it during + // app creation (bootstrapLogging) and during loggers configuration (DefaultLoggingFactory) + // So it must be re-attached both just after app creation and in the run phase + recorder.attach(); + + return recorder.getRecordedLogs(); + } + } + + /** + * Technical bundle used to re-attach log recorders after dropwizard resets all loggers. + */ + public final class RecordedLogsTrackingBundle implements ConfiguredBundle { + @Override + public void run(final Configuration configuration, + final Environment environment) throws Exception { + recorders.forEach(Recorder::attach); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordedLogs.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordedLogs.java new file mode 100644 index 000000000..8e3292700 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/RecordedLogs.java @@ -0,0 +1,60 @@ +package ru.vyarus.dropwizard.guice.test.log; + +/** + * Recorded logs access object for {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs}. + * It might be one or multiple tracked loggers (or could be all logs if exact loggers were not configured). + *

        + * To avoid tons of selection methods with different parameters, all selection methods + * return sub-selector object for further selections. For example, to select messages by level and logger: + * {@code logger(SomeClass.class).level(Level.DEBUG).messages()}. + *

        + * Terminator methods: + *

          + *
        • {@link #count()} + *
        • {@link #empty()} + *
        • {@link #events()} + *
        • {@link #messages()} (or generic {@link #messages(java.util.function.Function)}) + *
        • {@link #has(org.slf4j.event.Level)} + *
        • {@link #has(Class)} + *
        + *

        + * Sub selects: + *

          + *
        • {@link #level(org.slf4j.event.Level...)} + *
        • {@link #logger(Class[])} + *
        • {@link #containing(String)} + *
        • {@link #matching(String)} + *
        • {@link #select(java.util.function.Predicate)} (generic) + *
        + * + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class RecordedLogs extends LogsSelector { + + private final Recorder recorder; + + /** + * Create recorded logs accessor. + * + * @param recorder recorder + */ + public RecordedLogs(final Recorder recorder) { + super(recorder.getRecords()); + this.recorder = recorder; + } + + /** + * Clear collected recordings. + */ + public void clear() { + recorder.clear(); + } + + /** + * @return recorder object, used to attach and detach log handlers. + */ + public Recorder getRecorder() { + return recorder; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/Recorder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/Recorder.java new file mode 100644 index 000000000..666a0a7c2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/log/Recorder.java @@ -0,0 +1,122 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.filter.ThresholdFilter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Logs recorder. Applies custom appender for logback logger(s) and change level, if required. Collect all + * log events. + * + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class Recorder { + private final ListAppender appender = new ListAppender<>(); + private final List loggers; + private final Level level; + + /** + * Create recorder. + * + * @param name name + * @param level base level + * @param loggers target loggers + */ + public Recorder(final String name, final org.slf4j.event.Level level, final List loggers) { + this.loggers = loggers; + this.level = Level.toLevel(level.toString()); + // custom name (field name) to clearly see custom appender + appender.setName(name); + + final ThresholdFilter levelFilter = new ThresholdFilter(); + levelFilter.setLevel(level.toString()); + levelFilter.start(); + + appender.addFilter(levelFilter); + } + + /** + * @return raw recorded records + * @see #getRecordedLogs() for easy navigation + */ + public List getRecords() { + return appender.list; + } + + /** + * Note that object always returns actual logs. So the same instance could be used to all verifications. + * + * @return recorded logs navigation object + */ + public RecordedLogs getRecordedLogs() { + return new RecordedLogs(this); + } + + /** + * Clear recordings. + */ + public void clear() { + appender.list.clear(); + } + + /** + * Could be called multiple times. Initially called before application start to record all startup events. + * But dropwizard reset loggers just before run phase, so appender must be applied second time. + *

        + * DON'T call this method manually: simply no need - hook will call it in appropriate moments. + */ + public void attach() { + final boolean rootLevelMatch = isRootLevelMatch(); + getMatchedLoggers().forEach(logger -> { + if (!logger.isAttached(appender)) { + logger.addAppender(appender); + } + // lower logger level to receive all required events + // if logger does not have level - see root logger (root loger threshold might be lower than required + // so it would be not correct to set level at any case) + if ((logger.getLevel() != null && !isLevelMatch(logger)) || !rootLevelMatch) { + logger.setLevel(level); + } + }); + appender.start(); + } + + /** + * Remove appender from logger. + *

        + * Not required as dropwizard reset all logging during application startup + * and so stale appenders would be removed in any case before each new test. + */ + public void destroy() { + appender.stop(); + getMatchedLoggers().forEach(logger -> logger.detachAppender(appender)); + } + + private boolean isRootLevelMatch() { + // check if the root logger level is lower than required + return isLevelMatch((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)); + } + + private boolean isLevelMatch(final Logger logger) { + final Level currentLevel = logger.getLevel(); + // if level not set look root logger level to match (it will define the logged threshold) + return currentLevel != null && (currentLevel.equals(level) || !currentLevel.isGreaterOrEqual(level)); + } + + private List getMatchedLoggers() { + final List res = new ArrayList<>(); + if (loggers.isEmpty()) { + res.add((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)); + } else { + loggers.forEach(loggerName -> res.add((Logger) LoggerFactory.getLogger(loggerName))); + } + return res; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/mock/MocksHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/mock/MocksHook.java new file mode 100644 index 000000000..eae3c7931 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/mock/MocksHook.java @@ -0,0 +1,102 @@ +package ru.vyarus.dropwizard.guice.test.mock; + +import com.google.common.base.Preconditions; +import com.google.inject.Binder; +import org.mockito.Mockito; +import org.mockito.internal.util.MockUtil; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.util.HashMap; +import java.util.Map; + +/** + * Replace any guice service with mockito mock in test (using guice module overrides). + *

        + * Important: requires mockito dependency! + *

        + * Usage example: + *

        
        + *  MocksHook hook = new MocksHook();
        + *  Service mock = hook.mock(Service.class);
        + *  when(mock.foo()).thenReturn(..)
        + * 
        + *

        + * Could also be used for spy objects registration of beans bound by instance (!) + * (so {@link ru.vyarus.dropwizard.guice.test.spy.SpiesHook} could not be used): + * {@code Service spy = hook.mock(Service.class, Mockito.spy(new Service()))}. + * Spy should also be used when mock must be created from an abstract class (preserving abstract methods): + * {@code AbstractService mock = hook.mock(AbstractService.class, Mockito.spy(AbstractService.class))}. + *

        + * Limitation: any aop, applied to the original bean, will not work with mock (because guice can't apply aop to + * instances)! Use {@link ru.vyarus.dropwizard.guice.test.spy.SpiesHook} instead if aop is important. + * Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 29.04.2025 + */ +public class MocksHook implements GuiceyConfigurationHook { + + private final Map, Object> mocks = new HashMap<>(); + private boolean initialized; + + @Override + public void configure(final GuiceBundle.Builder builder) throws Exception { + if (!mocks.isEmpty()) { + builder.modulesOverride(binder -> + mocks.forEach((aClass, o) -> bindMock(binder, aClass, o))); + } + initialized = true; + } + + /** + * Override gucie bean with a mock instance. + * + * @param type bean type + * @param bean type + * @return mock instance + */ + public T mock(final Class type) { + return mock(type, Mockito.mock(type)); + } + + /** + * Override guice bean with a user-provided mock instance. + * + * @param type bean type + * @param mock mock instance + * @param bean type + * @return passed mock instance + */ + public T mock(final Class type, final T mock) { + Preconditions.checkState(!initialized, "Too late. Mocks already applied."); + Preconditions.checkState(MockUtil.isMock(mock), "Provided object is not a mockito mock object."); + Preconditions.checkState(!mocks.containsKey(type), "Mock object for type %s is already registered.", + type.getSimpleName()); + mocks.put(type, mock); + return mock; + } + + /** + * @param type bean type + * @param bean type + * @return mock instance registered for bean type + * @throws java.lang.IllegalStateException if mock for type is not registered + */ + @SuppressWarnings("unchecked") + public T getMock(final Class type) { + return (T) Preconditions.checkNotNull(mocks.get(type), "Mock not registered for type %s", type.getSimpleName()); + } + + /** + * Reset all registered mocks. + */ + public void resetMocks() { + mocks.values().forEach(Mockito::reset); + } + + @SuppressWarnings("unchecked") + private void bindMock(final Binder binder, final Class type, final Object mock) { + binder.bind((Class) type).toInstance((K) mock); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestClient.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestClient.java new file mode 100644 index 000000000..37a9dad59 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestClient.java @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.WebTarget; +import org.glassfish.jersey.test.JerseyTest; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.rest.support.GuiceyJerseyTest; + +import static java.util.Objects.requireNonNull; + +/** + * REST client for test stubbed rest ({@link ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest}). + *

        + * {@link #client()} provides a raw client, configured with: + * - Random port + * - Requests logging ({@code @StubRest(logRequests = true)}, enabled by default) + * - Enabled restricted headers and method workaround (for url connection, used by in-memory test container) + * - Set default timeouts to avoid infinite calls + * - Enabled multipart support (if available in classpath) + *

        + * Provides the same abilities as a client from {@link ru.vyarus.dropwizard.guice.test.ClientSupport}. + *

        + * {@inheritDoc} + *

        + * By default, defaults are reset after each test. So defaults could be specified in the test setup method (to apply + * the same for all tests in class) or just before method call (in method test directly). Automatic rest could be + * disabled with {@code @StubRest(autoReset = false)}. + * + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class RestClient extends TestClient { + + private final GuiceyJerseyTest jerseyTest; + /** + * Create a client. + * + * @param jerseyTest jersey test instance + */ + public RestClient(final GuiceyJerseyTest jerseyTest) { + super(null); + this.jerseyTest = jerseyTest; + } + + /** + * Returns the pre-configured {@link jakarta.ws.rs.client.Client} for this test. + * + * @return the {@link JerseyTest} configured {@link jakarta.ws.rs.client.Client} + */ + public Client client() { + return getJerseyTest().client(); + } + + @Override + protected WebTarget getRoot() { + return getJerseyTest().target(); + } + + private GuiceyJerseyTest getJerseyTest() { + return requireNonNull(jerseyTest); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestStubsHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestStubsHook.java new file mode 100644 index 000000000..a9e8ca7ef --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/RestStubsHook.java @@ -0,0 +1,330 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import io.dropwizard.core.setup.Environment; +import io.dropwizard.core.setup.ExceptionMapperBinder; +import io.dropwizard.jersey.jackson.JacksonFeature; +import io.dropwizard.jersey.validation.HibernateValidationBinder; +import jakarta.servlet.DispatcherType; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.GuiceyOptions; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.context.Disables; +import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartingEvent; +import ru.vyarus.dropwizard.guice.test.rest.support.GuiceyJerseyTest; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.function.Predicate; + +/** + * Resources stubbing: start lightweight rest container (with, probably, only one or a couple of services + * to test) without web (no servlets, filters, etc. would work). This is the same as dropwizard's + * {@link io.dropwizard.testing.junit5.ResourceExtension}, but with full guice support. Rest extensions like exception + * mappers, filters, etc. could also be disabled (including dropwizard default extensions). As guicey knows all + * registered extensions, it provides them automatically (so, by default, no configuration is required - all jersey + * resources and extensions are available). + *

        + * It is not quite correct to call it stubs - because this is a fully functional rest (same as in normal application). + * It is called stub just to highlight customization ability (for example, we can start only resource with just a + * bunch of enabled extensions). + *

        + * Could be used ONLY with lightweight guicey test + * ({@link ru.vyarus.dropwizard.guice.test.TestSupport#runCoreApp(Class, String, String...)}. + * Activates custom rest container, started on random port and with all rest resources and extensions. As test + * container does not support web resources (will simply not work), all registered web extensions are disabled + * (to avoid confusion by console output), together with {@link com.google.inject.servlet.GuiceFilter}. + *

        + * Rest client should be used to call rest: {@link #getRestClient()}. Use it to call rest methods: + * {@code Something result = rest.get("/relative/rest/path", Something.class)} (see + * {@link ru.vyarus.dropwizard.guice.test.rest.RestClient} class for usage info). + *

        + * To limit started rest resources, simply specify what resources to start (test could start only one resource to + * test it): {@code RestStubsRunner.builder().resources(Resources1.class, Resource2.class)}. Alternatively, + * if many resources required, you can disable some resources: + * {@code RestStubsRunner.builder().disableResources(Resources1.class, Resource2.class)}. + *

        + * By default, all jersey extensions, declared in application are applied. You can disable all of them: + * {@code @RestStubsRunner.builder().disableAllJerseyExtensions(true)} (note that dropwizard extensions remain!). + * Or you can specify just required extensions: + * {@code RestStubsRunner.builder().jerseyExtensions(Ext1.class, Ext2.class)}. + * Also, only some extensions could be disabled: + * {@code RestStubsRunner.builder().disableJerseyExtensions(Ext1.class, Ext2.class)}. + *

        + * Default dropwizard's exception mappers could be disabled with: + * {@code RestStubsRunner.builder().disableDropwizardExceptionMappers(true)}. This is very useful for testing rest + * errors (to receive exception instead of generic 500 response). + *

        + * By default, in-memory container (lightweight, but not all features supported) would be used and grizzly container, + * if available in classpath. Use {@code RestStubsRunner.builder().container(..)} option to force the exact container + * type (prevent incorrect usage). + *

        + * The full list of enabled jersey extensions (including dropwizard and jersey core) could be seen with + * {@code .printJerseyConfig()} option, activated in application (guice builder) or using a hook. + *

        + * Log requests option ({@code RestStubsRunner.builder().logRequests(true)} activates complete requests and responses + * logging. + *

        + * Warn: the default guicey client ({@link ru.vyarus.dropwizard.guice.test.ClientSupport}) would not work - but you + * don't need it as a complete rest client provided. + * + * @author Vyacheslav Rusakov + * @since 20.04.2025 + */ +public class RestStubsHook implements GuiceyConfigurationHook { + + private final StubRestConfig config; + private GuiceyJerseyTest jerseyStub; + private RestClient restClient; + + /** + * Create hook. + * + * @param config configuration + */ + public RestStubsHook(final StubRestConfig config) { + this.config = config; + } + + /** + * @return builder to configure rest + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public void configure(final GuiceBundle.Builder builder) throws Exception { + builder + // disable guice filter (it wouldn't work anyway) + .option(GuiceyOptions.GuiceFilterRegistration, EnumSet.noneOf(DispatcherType.class)) + // disable all web extensions not working with test rest (they are just ignored - disabling only + // to indicate) + .disable(Disables.webExtension().and(Disables.jerseyExtension().negate())) + + // started just before lifecycle startup (even if managed beans processing will be disabled, + // lifecycle events will work). Important to run before ApplicationStarted even which is often + // used by reporters + .listen(event -> { + if (event.getType().equals(GuiceyLifecycle.ApplicationStarting)) { + final ApplicationStartingEvent evt = (ApplicationStartingEvent) event; + + // manual registration required to reproduce production environment + registerDropwizardExtensions(evt.getEnvironment(), + config.isDisableDropwizardExceptionMappers()); + + start(config, evt.getEnvironment()); + } + }) + .onApplicationShutdown(injector -> stop()); + + disableResources(config, builder); + disableJerseyExtensions(config, builder); + } + + /** + * @return jersey test instance + */ + public GuiceyJerseyTest getJerseyStub() { + return jerseyStub; + } + + /** + * @return rest client, configured to call stubbed rest + */ + public RestClient getRestClient() { + return restClient; + } + + // manual registration required to reproduce production environment see + // io.dropwizard.testing.common.DropwizardTestResourceConfig.DropwizardTestResourceConfig + private void registerDropwizardExtensions(final Environment environment, + final boolean disableExceptionMappers) { + // it might be more convenient to verify exceptions directly, instead of 500 responses + if (!disableExceptionMappers) { + environment.jersey().register(new ExceptionMapperBinder(false)); + } + environment.jersey().register(new JacksonFeature(environment.getObjectMapper())); + environment.jersey().register(new HibernateValidationBinder(environment.getValidator())); + } + + private void start(final StubRestConfig config, final Environment environment) { + + jerseyStub = GuiceyJerseyTest.create(environment, config.getContainer(), config.isLogRequests()); + try { + jerseyStub.setUp(); + } catch (Exception e) { + throw new IllegalStateException("Failed to start test jersey container", e); + } + restClient = new RestClient(jerseyStub); + } + + private void stop() { + if (jerseyStub != null) { + try { + jerseyStub.tearDown(); + } catch (Exception e) { + throw new IllegalStateException("Failed to shutdown test jersey container", e); + } + } + } + + private void disableResources(final StubRestConfig config, final GuiceBundle.Builder builder) { + Predicate resourcesDisable = null; + if (!config.getResources().isEmpty()) { + // disable all except declared types + resourcesDisable = Disables.installedBy(ResourceInstaller.class) + .and(Disables.type(config.getResources().toArray(Class[]::new)).negate()); + } else if (!config.getDisableResources().isEmpty()) { + // disable declared + resourcesDisable = Disables.installedBy(ResourceInstaller.class) + .and(Disables.type(config.getDisableResources().toArray(Class[]::new))); + } + if (resourcesDisable != null) { + builder.disable(resourcesDisable); + } + } + + private void disableJerseyExtensions(final StubRestConfig config, final GuiceBundle.Builder builder) { + Predicate extDisable = null; + if (!config.getJerseyExtensions().isEmpty()) { + // disable all except declared types + extDisable = Disables.jerseyExtension().and(Disables.installedBy(ResourceInstaller.class).negate()) + .and(Disables.type(config.getJerseyExtensions().toArray(Class[]::new)).negate()); + + } else if (config.isDisableAllJerseyExtensions() || !config.getDisableJerseyExtensions().isEmpty()) { + extDisable = Disables.jerseyExtension().and(Disables.installedBy(ResourceInstaller.class).negate()); + if (!config.isDisableAllJerseyExtensions()) { + extDisable = extDisable.and(Disables.type(config.getDisableJerseyExtensions().toArray(Class[]::new))); + } + } + if (extDisable != null) { + builder.disable(extDisable); + } + } + + /** + * Rest stubs configuration builder. + */ + public static class Builder { + private final StubRestConfig config = new StubRestConfig(); + + /** + * By default, all resources would be available. Use this option to run a subset of resources. + * + * @param resources resources to use in staub + * @return builder instance for chained calls + * @see #disableResources(Class[]) to disable some default resources + */ + public Builder resources(final Class... resources) { + Collections.addAll(config.getResources(), resources); + return this; + } + + /** + * NOTE: if resources specified in {@link #resources(Class[])} then the disable option would be ignored (all + * required resources already specified). This option is useful to exclude only some resources from the + * registered application resources + *

        + * Important: affects only resources, recognized as guicey extensions. Manually registered resources + * would remain! + * + * @param disableResources resources to disable + * @return builder instance for chained calls + */ + public Builder disableResources(final Class... disableResources) { + Collections.addAll(config.getDisableResources(), disableResources); + return this; + } + + /** + * By default, all jersey extension, registered in application, would be registered. Use this option to specify + * exact required extensions (all other application extensions would be disabled). + *

        + * Important: this affects only guicey extensions (all other guicey extension would be simply disabled). + * To disable core dropwizard exception mappers use {@link #disableDropwizardExceptionMappers(boolean)}. + * + * @param jerseyExtensions jersey extensions to use in stub + * @return builder instance for chained calls + */ + public Builder jerseyExtensions(final Class... jerseyExtensions) { + Collections.addAll(config.getJerseyExtensions(), jerseyExtensions); + return this; + } + + /** + * NOTE: if extensions specified in {@link #jerseyExtensions(Class[])} then the disable option would be ignored + * (all required extensions already specified). + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #disableDropwizardExceptionMappers(boolean)} ()}. + * + * @param disableAllJerseyExtensions true to disable all application jersey extensions + * @return builder instance for chained calls + */ + public Builder disableAllJerseyExtensions(final boolean disableAllJerseyExtensions) { + config.setDisableAllJerseyExtensions(disableAllJerseyExtensions); + return this; + } + + /** + * By default, all dropwizard exception mappers registered (same as in real application). For tests, it might be + * more convenient to disable them and receive direct exception objects after test. + * + * @param disableDropwizardExceptionMappers true dropwizard exception mappers + * @return builder instance for chained calls + */ + public Builder disableDropwizardExceptionMappers(final boolean disableDropwizardExceptionMappers) { + config.setDisableDropwizardExceptionMappers(disableDropwizardExceptionMappers); + return this; + } + + /** + * NOTE: if extensions specified in {@link #jerseyExtensions(Class[])} then the disable option would be ignored + * (all required extensions already specified). This option is useful to exclude only some extensions from the + * registered application jersey extensions. + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #disableDropwizardExceptionMappers(boolean)}. + * + * @param disableJerseyExtensions jersey extensions to disable + * @return builder instance for chained calls + */ + public Builder disableJerseyExtensions(final Class... disableJerseyExtensions) { + Collections.addAll(config.getDisableJerseyExtensions(), disableJerseyExtensions); + return this; + } + + /** + * Requests log enabled by default (like in {@link ru.vyarus.dropwizard.guice.test.ClientSupport}). + * + * @param logRequests true to print all requests and responses into console + * @return builder instance for chained calls + */ + public Builder logRequests(final boolean logRequests) { + config.setLogRequests(logRequests); + return this; + } + + /** + * By default, use a lightweight in-memory container, but switch to grizzly when it's available in classpath + * (this is the default behavior of {@link org.glassfish.jersey.test.JerseyTest}). + * + * @param policy required test container policy + * @return builder instance for chained calls + */ + public Builder container(final TestContainerPolicy policy) { + config.setContainer(policy); + return this; + } + + /** + * @return configured hook + */ + public RestStubsHook build() { + return new RestStubsHook(config); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/StubRestConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/StubRestConfig.java new file mode 100644 index 000000000..5036c42e8 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/StubRestConfig.java @@ -0,0 +1,143 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stub rest configuration for {@link RestStubsHook}. + * + * @author Vyacheslav Rusakov + * @since 20.04.2025 + */ +public class StubRestConfig { + + private final List> resources = new ArrayList<>(); + private final List> disableResources = new ArrayList<>(); + private final List> jerseyExtensions = new ArrayList<>(); + private boolean disableAllJerseyExtensions; + private boolean disableDropwizardExceptionMappers; + private final List> disableJerseyExtensions = new ArrayList<>(); + private boolean logRequests = true; + private TestContainerPolicy container = TestContainerPolicy.DEFAULT; + + /** + * By default, all resources would be available. Use this option to run a subset of resources. + * + * @return resources to use in stub + * @see #getDisableResources() to disable some default resources + */ + public List> getResources() { + return resources; + } + + /** + * NOTE: if resources specified in {@link #getResources()} then the disable option would be ignored (all required + * resources already specified). This option is useful to exclude only some resources from the registered + * application resources + *

        + * Important: affects only resources, recognized as guicey extensions. Manually registered resources + * would remain! + * + * @return resources to disable + */ + public List> getDisableResources() { + return disableResources; + } + + /** + * By default, all jersey extension, registered in application, would be registered. Use this option to specify + * exact required extensions (all other application extensions would be disabled). + *

        + * Important: this affects only guicey extensions (all other guicey extension would be simply disabled). + * To disable core dropwizard exception mappers use {@link #setDisableDropwizardExceptionMappers(boolean)}. + * + * @return jersey extensions to use in stub + */ + public List> getJerseyExtensions() { + return jerseyExtensions; + } + + /** + * @return true to disable all application jersey extensions + */ + public boolean isDisableAllJerseyExtensions() { + return disableAllJerseyExtensions; + } + + /** + * NOTE: if extensions specified in {@link #getJerseyExtensions()} then the disable option would be ignored (all + * required extensions already specified). + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #isDisableDropwizardExceptionMappers()}. + * + * @param disableAllJerseyExtensions true to disable all application jersey extensions + */ + public void setDisableAllJerseyExtensions(final boolean disableAllJerseyExtensions) { + this.disableAllJerseyExtensions = disableAllJerseyExtensions; + } + + /** + * @return true dropwizard exception mappers + */ + public boolean isDisableDropwizardExceptionMappers() { + return disableDropwizardExceptionMappers; + } + + /** + * By default, all dropwizard exception mappers registered (same as in real application). For tests, it might be + * more convenient to disable them and receive direct exception objects after test. + * + * @param disableDropwizardExceptionMappers true dropwizard exception mappers + */ + public void setDisableDropwizardExceptionMappers(final boolean disableDropwizardExceptionMappers) { + this.disableDropwizardExceptionMappers = disableDropwizardExceptionMappers; + } + + /** + * NOTE: if extensions specified in {@link #getJerseyExtensions()} then the disable option would be ignored (all + * required extensions already specified). This option is useful to exclude only some extensions from the registered + * application jersey extensions. + *

        + * Does not affect dropwizard default extensions (only affects extension, controlled by guicey). + * Dropwizard exception mappers could be disabled with {@link #isDisableDropwizardExceptionMappers()}. + * + * @return jersey extensions to disable + */ + public List> getDisableJerseyExtensions() { + return disableJerseyExtensions; + } + + /** + * @return true to print all requests and responses into console + */ + public boolean isLogRequests() { + return logRequests; + } + + /** + * Requests log enabled by default (like in {@link ru.vyarus.dropwizard.guice.test.ClientSupport}). + * + * @param logRequests true to print all requests and responses into console + */ + public void setLogRequests(final boolean logRequests) { + this.logRequests = logRequests; + } + + /** + * @return required test container policy + */ + public TestContainerPolicy getContainer() { + return container; + } + + /** + * By default, use a lightweight in-memory container, but switch to grizzly when it's available in classpath + * (this is the default behavior of {@link org.glassfish.jersey.test.JerseyTest}). + * + * @param container required test container policy + */ + public void setContainer(final TestContainerPolicy container) { + this.container = container; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/TestContainerPolicy.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/TestContainerPolicy.java new file mode 100644 index 000000000..99cab0ff2 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/TestContainerPolicy.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +/** + * Jersey test container implementation selection policy. + * + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +public enum TestContainerPolicy { + + /** + * Use grizzly, if available in classpath, otherwise use in memory container. Also, factory could be specified + * in {@link org.glassfish.jersey.test.TestProperties#CONTAINER_FACTORY} system property (see + * {@link org.glassfish.jersey.test.JerseyTest#getDefaultTestContainerFactory()}) + */ + DEFAULT, + /** + * Use {@link org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory}, even if grizzly is available in + * classpath. In-memory is a lightweight rest container, but it does not support all features. If not supported + * features requires - use grizzly. + */ + IN_MEMORY, + /** + * Use {@code org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory}. Fail with a descriptive message + * if factory is not found in classpath. Useful to prevent accident in-memory container usage. + */ + GRIZZLY +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/ExtensionsSelector.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/ExtensionsSelector.java new file mode 100644 index 000000000..00b845218 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/ExtensionsSelector.java @@ -0,0 +1,76 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.module.context.Filters; +import ru.vyarus.dropwizard.guice.module.context.info.ItemId; +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Helper class to implement extension selector from guicey info. + * + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +public class ExtensionsSelector { + + private final GuiceyConfigurationInfo info; + + /** + * Create selector. + * + * @param info guicey info + */ + public ExtensionsSelector(final GuiceyConfigurationInfo info) { + this.info = info; + } + + /** + * @return list of enabled rest resources + */ + public List> getResources() { + return info.getData().getItems(Filters.extensions() + .and(Filters.installedBy(ResourceInstaller.class)) + .and(Filters.enabled())) + .stream() + .map(ItemId::getType) + .sorted(Comparator.comparing(Class::getSimpleName)) + .collect(Collectors.toList()); + } + + /** + * @return count of disabled rest resources + */ + public int getDisabledResourcesCount() { + return info.getData().getItems(Filters.extensions() + .and(Filters.installedBy(ResourceInstaller.class)) + .and(Filters.disabled())).size(); + } + + /** + * @return list of enabled jersey extensions + */ + public List> getExtensions() { + return info.getData().getItems(Filters.extensions() + .and(Filters.jerseyExtension() + .and(Filters.installedBy(ResourceInstaller.class).negate())) + .and(Filters.enabled())) + .stream() + .map(ItemId::getType) + .sorted(Comparator.comparing(Class::getSimpleName)) + .collect(Collectors.toList()); + } + + /** + * @return count of disabled jersey extensions + */ + public int getDisabledExtensionsCount() { + return info.getData().getItems(Filters.extensions() + .and(Filters.jerseyExtension() + .and(Filters.installedBy(ResourceInstaller.class).negate())) + .and(Filters.disabled())).size(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/GuiceyJerseyTest.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/GuiceyJerseyTest.java new file mode 100644 index 000000000..32697ede1 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/rest/support/GuiceyJerseyTest.java @@ -0,0 +1,148 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.dropwizard.core.setup.Environment; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.internal.ServiceFinder; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; +import ru.vyarus.dropwizard.guice.test.client.util.MultipartCheck; +import ru.vyarus.dropwizard.guice.test.rest.TestContainerPolicy; + +import java.net.URI; +import java.util.logging.Level; +import java.util.stream.StreamSupport; + +/** + * Jersey rest stubs implementation (based on {@link org.glassfish.jersey.test.JerseyTest}). + * Configures: + *

          + *
        • Random port + *
        • Requests logging + *
        • Enables restricted headers and method workaround (for url connection) + *
        • Set default timeouts to avoid infinite calls + *
        • Enable multipart support (if available in classpath) + *
        + *

        + * Application deployment context used (same as in normal dropwizard application). Guicey disables not wanted + * extensions, if required. + *

        + * Assume 2 possible containers: in-memory (may not support some rest features) and grizzly. + * By default, should delegate container selection to {@link org.glassfish.jersey.test.JerseyTest}, which + * selects grizzly, if available or use in-memory. Also, supports custom system property. + * + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +public class GuiceyJerseyTest extends JerseyTest { + + private static Environment environment; + private static TestContainerPolicy policy; + private final boolean logRequests; + + /** + * Create jersey test. + * NOTE Environment can't be used in constructor directly due to configureDeployment() override + * + * @param logRequests true to log requests and responses + */ + protected GuiceyJerseyTest(final boolean logRequests) { + this.logRequests = logRequests; + + // allow restricted headers by default + // https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/client.html#d0e5292 + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + } + + /** + * @param environment environment + * @param policy container policy + * @param logRequests log requests and responses + * @return jersey test instance + */ + @SuppressFBWarnings("EI_EXPOSE_STATIC_REP2") + public static GuiceyJerseyTest create(final Environment environment, + final TestContainerPolicy policy, + final boolean logRequests) { + synchronized (GuiceyJerseyTest.class) { + // have to use static variable because environment requested from super constructor! + GuiceyJerseyTest.environment = environment; + GuiceyJerseyTest.policy = policy; + return new GuiceyJerseyTest(logRequests); + } + } + + @Override + @SuppressWarnings("PMD.ExhaustiveSwitchHasDefault") + public TestContainerFactory getTestContainerFactory() { + final TestContainerFactory res; + switch (policy) { + case DEFAULT: + res = super.getTestContainerFactory(); + break; + case IN_MEMORY: + res = new InMemoryTestContainerFactory(); + break; + case GRIZZLY: + // use service loader to load available factories + res = StreamSupport.stream(ServiceFinder + .find(TestContainerFactory.class).spliterator(), false) + .filter(factory -> + "org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory" + .equals(factory.getClass().getName())) + .findFirst().orElseThrow(() -> new IllegalStateException( + "org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory is not available in " + + "classpath. Add `org.glassfish.jersey.test-framework.providers:jersey-test-" + + "framework-provider-grizzly2` dependency (version managed by dropwizard BOM)" + )); + break; + default: + throw new IllegalStateException("Unsupported policy: " + policy); + } + return res; + } + + @Override + protected URI getBaseUri() { + // can't be in constructor - too late + forceSet(TestProperties.CONTAINER_PORT, "0"); + return super.getBaseUri(); + } + + @Override + protected DeploymentContext configureDeployment() { + // NOTE: called from super constructor and so can't see variables! + return ServletDeploymentContext + // use application config (almost the same as with normal startup) + .builder(environment.jersey().getResourceConfig()) + .build(); + } + + @Override + protected void configureClient(final ClientConfig clientConfig) { + // log everything to simplify debug + if (logRequests) { + clientConfig.register(LoggingFeature.builder() + .withLogger(new DefaultTestClientFactory.ConsoleLogger()) + .verbosity(LoggingFeature.Verbosity.PAYLOAD_ANY) + .level(Level.INFO) + .build()); + } + // prevent infinite loading + clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 1000); + clientConfig.property(ClientProperties.READ_TIMEOUT, 5000); + // https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/client.html#d0e5292 + clientConfig.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); + + // when dropwizard-forms used, automatically register multipart feature + MultipartCheck.getMultipartFeatureClass().ifPresent(clientConfig::register); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpiesHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpiesHook.java new file mode 100644 index 000000000..036364b4f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpiesHook.java @@ -0,0 +1,125 @@ +package ru.vyarus.dropwizard.guice.test.spy; + +import com.google.common.base.Preconditions; +import com.google.inject.matcher.Matchers; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.util.HashMap; +import java.util.Map; + +/** + * Replace any guice service with mockito spy. The difference with mock: spy wraps around the real service(!) + * and could be used to validate called service methods (verify incoming parameters and output value). + *

        + * Important: requires mockito dependency! + *

        + * In contrast to mocks and stubs, spies work with guice AOP: all calls to service are intercepted and + * passed through the spy object (as "proxy"). That also means that all aop, applied to the original bean, would + * work (in contrast to mocks). + *

        + * As spy requires real bean instance - spy object is created just after injector creation (and AOP interceptor + * redirects into it (then real bean called)). Spy object is different instance as injected bean + * ({@code @Inject SpiedService}) because injected bean would be a proxy, handling guice AOP. + *

        + * Calling bean methods directly on spy is completely normal (guice bean just redirects calls to spy object)! + *

        + * Usage example: + *

        
        + *     SpiesHook hook = new SpiesHook();
        + *     SpyProxy<Service> proxy = hook.spy(Service.class)
        + *     // actual spy object can be obtained only after guice application startup
        + *     Service spy = proxy.getSpy()
        + *     doReturn(12).when(spy).foo();
        + * 
        + *

        + * Alternatively, provider might be used instead of proxy type (for simplicity): + * {@code Provider provider = hook.spy(Service.class)} + *

        + * Spy CAN'T be initialized manually. Use {@link ru.vyarus.dropwizard.guice.test.mock.MocksHook} + * or {@link ru.vyarus.dropwizard.guice.test.stub.StubsHook} + * instead for manual spy objects initialization. Such manual initialization could be required for spies created + * from abstract classes (or multiple interfaces): in this case actual bean instance is not required, and so mocks + * support could be used instead: + * {@code AbstractService spy = mocksHook.mock(AbstractService.class, Mockito.spy(AbstractService.class))}. + *

        + * Actual spy object instance is created only on first bean access (after or in time of application startup). + * Normally, it is ok to wait for application startup, configure spy object and then run tests methods (using + * spy). But if spied bean is involved in application startup (called by some managed objects) then the only + * way to configure it is to apply modification just after spy instance creation: + * {@code hook.spy(Service.class).withInitializer(spy -> { doReturn(12).when(spy).foo() })}. + * + * @author Vyacheslav Rusakov + * @since 29.04.2025 + */ +public class SpiesHook implements GuiceyConfigurationHook { + + private final Map, SpyProxy> spies = new HashMap<>(); + private boolean initialized; + + @Override + @SuppressWarnings("unchecked") + public void configure(final GuiceBundle.Builder builder) throws Exception { + if (!spies.isEmpty()) { + builder.modulesOverride(binder -> { + spies.forEach((type, spy) -> { + spy.setInstanceProvider(binder.getProvider(type)); + // real binding isn't overridden, just used aop to intercept call and redirect into spy + binder.bindInterceptor(Matchers.only(type), Matchers.any(), spy); + }); + }); + } + initialized = true; + } + + /** + * Request wrapping target bean with a spy. + *

        + * As spy object requires bean instance, then spy could be created only during injector startup. Returned proxy + * must be used to get an actual spy object after application startup (for configuration before test logic + * execution). + *

        + * Returned proxy instance could be used for startup initializer registration: + * {@code hook.spy(Service.class).withInitializer(...)} (required ONLY in cases when spy must be used during + * application startup and so can't be configured after application startup. + *

        + * Returned proxy also implements {@code Provider} interface, so provider could be used instead of proxy type: + * {@code Provider provider = hook.spy(Service.class)} + *

        + * For spies targeting guice beans registered by instance use mocks + * ({@link ru.vyarus.dropwizard.guice.test.mock.MocksHook}) because in this case bean instance is not required. + * + * @param type bean type + * @return spy proxy instance + * @param bean type + */ + public SpyProxy spy(final Class type) { + Preconditions.checkState(!initialized, "Too late. Spies already applied."); + Preconditions.checkState(!spies.containsKey(type), "Mock object for type %s is already registered.", + type.getSimpleName()); + final SpyProxy spy = new SpyProxy<>(type); + spies.put(type, spy); + return spy; + } + + /** + * @param type bean type + * @return mockito spy object (not proxy!) + * @param bean type + * @throws java.lang.IllegalStateException if spy for bean is not registered + */ + @SuppressWarnings("unchecked") + public T getSpy(final Class type) { + return (T) Preconditions.checkNotNull( + spies.get(type), "Spy not registered for type %s", type.getSimpleName()) + .getSpy(); + } + + /** + * Reset all registered spies. + */ + public void resetSpies() { + spies.values().forEach(spyProxy -> Mockito.reset(spyProxy.getSpy())); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpyProxy.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpyProxy.java new file mode 100644 index 000000000..3d5606ace --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/spy/SpyProxy.java @@ -0,0 +1,115 @@ +package ru.vyarus.dropwizard.guice.test.spy; + +import com.google.common.base.Preconditions; +import com.google.inject.Provider; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * AOP interceptor redirect calls from the real bean into spy object, which was created around the same real bean. + *

        + * There is a chicken-egg problem: service binding can't be overridden (with spy instance), because spy requires + * service instance for construction. So, instead of replacing bean, we intercept bean calls. Actual spy object + * is created lazily just after injector creation. On the first call, AOP interceptor breaks the current aop chain + * (if other interceptors registered) and redirect calls to spy, which again calls the same service (including + * aop handler), but, this time, it processes normally. + * + * @param bean type + */ +public class SpyProxy implements MethodInterceptor, Provider { + private final Class type; + private final List> startupInitializers = new ArrayList<>(); + private Provider instanceProvider; + private volatile T spy; + + /** + * Create proxy. + * + * @param type bean type + */ + public SpyProxy(final Class type) { + this.type = type; + } + + /** + * Actual spy object instance is created only on first bean access (after or in time of application startup). + * Normally, it is ok to wait for application startup, configure spy object and then run tests methods (using + * spy). But if spied bean is involved in application startup (called by some managed objects) then the only + * way to configure it is to apply modification just after spy instance creation. + *

        + * Might be called multiple times (for multiple initializers configuration). + * + * @param initializer spy object initializer + * @return proxy instance + */ + public final SpyProxy withInitializer(final Consumer initializer) { + this.startupInitializers.add(initializer); + return this; + } + + /** + * Delayed bean instance provider. Required because a proxy object created before guice modules processing + * (provider could be obtained from binder). + * + * @param instanceProvider bean instance provider, used to get bean instance for spying + */ + public void setInstanceProvider(final Provider instanceProvider) { + Preconditions.checkState(this.instanceProvider == null, "Instance provider already set"); + this.instanceProvider = instanceProvider; + } + + /** + * @return proxied bean type + */ + public Class getType() { + return type; + } + + /** + * @return spy instance + */ + public synchronized T getSpy() { + if (spy == null) { + // lazy spy init + final T bean = Preconditions.checkNotNull(instanceProvider.get()); + spy = Mockito.spy(bean); + for (Consumer initializer : startupInitializers) { + initializer.accept(spy); + } + } + return spy; + } + + /** + * Alternative spy provider to use proxy as {@code Provider}. Internally, this method is not used as + * it is hard to search usages for it. + * + * @return spy instance + */ + @Override + public T get() { + return getSpy(); + } + + @Override + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public synchronized Object invoke(final MethodInvocation methodInvocation) throws Throwable { + // WARNING: for proper execution, this requires this AOP handler to be top most! + // (otherwise, some interceptors would be called multiple times) + + final boolean isSpyCalled = methodInvocation.getThis() == spy; + if (isSpyCalled) { + // second call (from spy) - normal execution, including all underlying aop + return methodInvocation.proceed(); + } + + // first call - interceptor breaks the AOP chain by calling the same method on spy object, which + // wraps the same proxied bean (so interceptor would be called second time) + return methodInvocation.getMethod().invoke(getSpy(), methodInvocation.getArguments()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubLifecycle.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubLifecycle.java new file mode 100644 index 000000000..706196998 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubLifecycle.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.stub; + +/** + * Helper interface for lifecycle-aware stubs implementation. Works with junit 5 + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean} extension + * and raw {@link ru.vyarus.dropwizard.guice.test.stub.StubsHook}. For stubs, implementing + * this interface, before and after methods would be called before and after each test (to perform some + * cleanups or reset state). + * + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public interface StubLifecycle { + + /** + * Called before each test method. + */ + default void before() { + // empty by default + } + + /** + * Called after each test method. + */ + default void after() { + // empty by default + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubsHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubsHook.java new file mode 100644 index 000000000..fb06c18e5 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/stub/StubsHook.java @@ -0,0 +1,152 @@ +package ru.vyarus.dropwizard.guice.test.stub; + +import com.google.common.base.Preconditions; +import com.google.inject.Binder; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.util.HashMap; +import java.util.Map; + +/** + * Replace any guice service with its stub in test (using guice module overrides). Consider stubs as a hand-made + * mocks. + *

        + * Example: suppose we have some {@code Service} and we need to modify it for tests, so we extend it with + * {@code class ServiceStub extends Service} and override required methods. Register stub in hook + * as {@code hook.stub(Service.class, ServiceStub.class)}. Internally, overriding guice binding would be created: + * {@code bind(Service.class).to(ServiceStub.class).in(Singleton.class)} so guice would + * create stub instead of the original service. Guice would create stub instance, so injections would work inside it + * (and AOP). + *

        + * Stub could also be initialized manually: manual instance would be injected into guice context (annotated fields + * injection would also be performed for provided instance): {@code hook.stub(Service.class, new ServiceStub())}. + *

        + * More canonical example with interface: suppose we have {@code bind(IServie.clas).to(ServiceImpl.class))}. In this + * case, stub could simply implement interface, instead of extending class + * ({@code class ServiceStub implements IService}): {@code hook.stub(IService.class, ServiceStub.class)}; + *

        + * Just in case: guice injection will also return stabbed bean (because stub instance is created by guice or + * instance bound into it). + *

        + * Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 30.04.2025 + */ +public class StubsHook implements GuiceyConfigurationHook { + + private final Map, Object> stubs = new HashMap<>(); + // used for lifecycle support implementation in guice-managed stubs + private final Map, Provider> stubProviders = new HashMap<>(); + private boolean initialized; + + @Override + public void configure(final GuiceBundle.Builder builder) throws Exception { + if (!stubs.isEmpty()) { + builder.modulesOverride(binder -> + stubs.forEach((aClass, o) -> bindStub(binder, aClass, o))); + } + initialized = true; + } + + /** + * Register stub class. Here stub must either extend original service ({@code class Stub extends Service}) or, + * if target service use interface for binding ({@code bind(IService.class).to(ServiceImpl.class}), implement + * that interface ({@code class Stub implements IService}). + *

        + * Stub instance would be managed with guice and so guice AOP could be applied for stub. + *

        + * If stub implements {@link ru.vyarus.dropwizard.guice.test.stub.StubLifecycle}, then stubs lifecycle could be + * emulated with {@link #before()} and {@link #after()} methods. Might be used to reset stub state between tests. + * + * @param type overriding service type + * @param stub stub implementation (used to override application service) + * @param service type + */ + public void stub(final Class type, final Class stub) { + Preconditions.checkState(!initialized, "Too late. Spies already applied."); + Preconditions.checkState(!type.equals(stub), "Stub must have a different type."); + Preconditions.checkState(!stubs.containsKey(type), "Stub object for type %s is already registered.", + type.getSimpleName()); + stubs.put(type, stub); + } + + /** + * Same as {@link #stub(Class, Class)}, but with manually created stub instance. In this case, guice AOP will not + * work for sub instance. {@code Binder.requestInjection(stub)} would be called for stub instance to support + * fields injection. + * + * @param type overriding service type + * @param value stub instance (used to override application service) + * @param service type + */ + public void stub(final Class type, final T value) { + Preconditions.checkState(!initialized, "Too late. Spies already applied."); + Preconditions.checkNotNull(value, "Stub cannot be null"); + Preconditions.checkState(!stubs.containsKey(type), "Stub object for type %s is already registered.", + type.getSimpleName()); + stubs.put(type, value); + } + + /** + * Run {@link StubLifecycle#before()} for all stubs, implementing lifecycle interface. + * For example, it could be called before each test. + */ + public void before() { + lifecycle(true); + } + + /** + * Run {@link StubLifecycle#after()} for all stubs, implementing lifecycle interface. + * For example, it could be called after each test. + */ + public void after() { + lifecycle(false); + } + + private void lifecycle(final boolean before) { + stubs.keySet().forEach(type -> { + final Object stub = getStub(type); + if (stub instanceof StubLifecycle) { + final StubLifecycle lifecycle = (StubLifecycle) stub; + if (before) { + lifecycle.before(); + } else { + lifecycle.after(); + } + } + }); + } + + /** + * @param type bean type + * @param bean type + * @param

        stub implementation type + * @return stub instance registered for bean type + * @throws java.lang.IllegalStateException if stub for type is not registered + */ + @SuppressWarnings("unchecked") + public P getStub(final Class type) { + final Object stub = Preconditions.checkNotNull(stubs.get(type), + "Stub not registered for type %s", type.getSimpleName()); + // stub instance might be guice-managed + return (P) (stub instanceof Class ? stubProviders.get(type).get() : stub); + } + + @SuppressWarnings("unchecked") + private void bindStub(final Binder binder, final Class type, final Object stub) { + if (stub instanceof Class) { + // bind original type to stub - guice will instantiate it + // IMPORTANT to bind as singleton - otherwise different instances would be everywhere + binder.bind((Class) type).to((Class) stub).in(Singleton.class); + // store guice-managed instance accessor to apply lifecycle + stubProviders.put(type, binder.getProvider(type)); + } else { + binder.requestInjection(stub); + binder.bind((Class) type).toInstance((K) stub); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/MethodTrack.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/MethodTrack.java new file mode 100644 index 000000000..0c83c3923 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/MethodTrack.java @@ -0,0 +1,254 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import com.codahale.metrics.Timer; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.lang.reflect.Method; +import java.time.Duration; + +/** + * Represents single method execution. Method arguments and the result stored as raw objects and in string form: + * this is required because raw objects might be mutable and could change in another method, before test verification + * would access them. Raw objects interception might be disabled, and so there would be nulls (string representations + * would always be). + *

        + * For the console output, it is preferred to wrap string values with quotes (to clearly see string bounds). Use + * {@link #getQuotedArguments()} and {@link #getQuotedResult()} for console output (as {@link #toStringTrack()} do). + *

        + * Bean instance, where method was called, is identified by unique hash: {@link #getInstanceHash()} (this is the same + * string as in default Object.toString (@something part)). Hash is required to detect method calls to different + * instances. + *

        + * The same metrics timer used to track calls of the same method (even for different objects). + * + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +@SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") +public class MethodTrack implements Comparable { + + private final Method method; + private final Class service; + private final String instanceHash; + private final long started; + private final Duration duration; + // arguments may contain non-primitive objects which could be modified after track recording, + // and so we can't rely on this for reporting + private final Object[] rawArguments; + private final String[] arguments; + private final Object rawResult; + private final String result; + private final Throwable throwable; + // indicates what arguments were strings to quote it in output (arguments + result) + private final boolean[] stringMarkers; + + // overall timer metric for all method executions (instance available in all tracks for convenience) + private final Timer timer; + + /** + * Create track. + * + * @param service service type + * @param method method + * @param instanceHash service instance hash + * @param started start time + * @param duration duration + * @param rawArguments raw arguments + * @param arguments string arguments + * @param rawResult raw result + * @param result string result + * @param throwable exception + * @param stringMarkers string arguments markers + * @param timer metrics + */ + @SuppressWarnings({"ParameterNumber", "PMD.ExcessiveParameterList", "PMD.ConstructorCallsOverridableMethod"}) + public MethodTrack(final Class service, + final Method method, + final String instanceHash, + final long started, + final Duration duration, + final Object[] rawArguments, + final String[] arguments, + final Object rawResult, + final String result, + final Throwable throwable, + final boolean[] stringMarkers, + final Timer timer) { + this.method = method; + this.service = service; + this.instanceHash = instanceHash; + this.started = started; + this.duration = duration; + this.rawArguments = rawArguments; + this.arguments = arguments; + this.rawResult = rawResult; + this.result = isVoidMethod() ? null : result; + this.throwable = throwable; + this.stringMarkers = stringMarkers; + this.timer = timer; + } + + /** + * @return called method + */ + public Method getMethod() { + return method; + } + + /** + * @return type of called guice bean + */ + public Class getService() { + return service; + } + + /** + * @return called instance hash + */ + public String getInstanceHash() { + return instanceHash; + } + + /** + * @return method start time + */ + public long getStarted() { + return started; + } + + /** + * @return method duration + */ + public Duration getDuration() { + return duration; + } + + /** + * WARNING: arguments could contain mutable objects, changed after method call (or even during the call) and so + * be careful when use it (e.g., for reporting) - values might not be actual. + * + * @return arguments used for method call or null if raw objects keeping disabled + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + public Object[] getRawArguments() { + return rawArguments; + } + + /** + * String representation details: + * - Primitive values, number and booleans stored as is + * - String values could be truncated (by default 30 chars allowed) + * - Objects represented as ObjectType@instanceHash + * - null is "null". + * + * @return string representation of method arguments + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + public String[] getArguments() { + return arguments; + } + + /** + * @return true if method is void (no return) + */ + public boolean isVoidMethod() { + return method.getReturnType().equals(void.class); + } + + /** + * WARNING: the result could be a mutable objects, changed after method call and so be careful when use it + * (e.g. for reporting) - value might not be exactly the same as returned value after the call. + * + * @return result object or null if raw objects keeping disabled + */ + public Object getRawResult() { + return rawResult; + } + + /** + * Note that if the method is void, a string result would also be null. Also, would be null if an error happened. + * + * @return string representation of the result + */ + public String getResult() { + return result; + } + + /** + * @return error thrown by method or null + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * @return true if no error appears, false otherwise + */ + public boolean isSuccess() { + return throwable == null; + } + + /** + * @return timer for all executions of this method (shared instance) + */ + public Timer getTimer() { + return timer; + } + + /** + * Almost the same as {@link #getArguments()}, but all string arguments wrapped with quotes (to see string bounds). + * + * @return string arguments for the console report + */ + @SuppressWarnings("PMD.UseStringBufferForStringAppends") + public String[] getQuotedArguments() { + final String[] res = new String[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + String arg = arguments[i]; + if (stringMarkers[i]) { + arg = "\"" + arg + "\""; + } + res[i] = arg; + } + return res; + } + + /** + * Almost the same as {@link #getResult()}, but, if the result returns string, quote it to see bounds. + * + * @return result string or null if method is void + */ + public String getQuotedResult() { + if (isVoidMethod()) { + return null; + } + return stringMarkers[arguments.length] ? "\"" + result + "\"" : result; + } + + /** + * @return string representation of method call (with arguments and return value) + */ + public String toStringTrack() { + // put all strings in quotes to make obvious not trimmed or empty strings + final StringBuilder res = new StringBuilder(method.getName()) + .append('(').append(String.join(", ", getQuotedArguments())).append(')'); + if (!isSuccess()) { + res.append(" ERROR ").append(throwable.getClass().getSimpleName()) + .append(": ").append(throwable.getMessage()); + } else if (!isVoidMethod()) { + res.append(" = ").append(getQuotedResult()); + } + return res.toString(); + } + + @Override + public String toString() { + return toStringTrack() + "\t (" + PrintUtils.ms(duration) + ")"; + } + + @Override + public int compareTo(final MethodTrack o) { + return Long.compare(started, o.getStarted()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/Tracker.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/Tracker.java new file mode 100644 index 000000000..48b8d8b05 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/Tracker.java @@ -0,0 +1,253 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.mockito.Mockito; +import org.mockito.internal.creation.DelegatingMethod; +import org.mockito.internal.debugging.LocationFactory; +import org.mockito.internal.invocation.InterceptedInvocation; +import org.mockito.internal.invocation.mockref.MockStrongReference; +import org.mockito.internal.progress.SequenceNumber; +import org.mockito.internal.stubbing.InvocationContainerImpl; +import org.mockito.internal.util.MockUtil; +import org.mockito.invocation.MatchableInvocation; +import org.mockito.stubbing.OngoingStubbing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.test.track.stat.TrackerStats; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Tracker object used for bean calls registration (together with + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean} or + * {@link ru.vyarus.dropwizard.guice.test.track.TrackersHook}). + *

        + * Use metrics timer to aggregate method calls statistics. Use {@link #getStats()} for report building. + *

        + * Tracked methods filtering implemented with mockito: {@link #findTracks(java.util.function.Function)}. + *

        + * By default, recorded tracks cleared after each test. + * + * @param bean type + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +@SuppressWarnings("ClassDataAbstractionCoupling") +public class Tracker { + private final Logger logger = LoggerFactory.getLogger(Tracker.class); + + private final Class type; + private final TrackerConfig config; + private final Duration warn; + private final List tracks = new CopyOnWriteArrayList<>(); + private final Map timers = new ConcurrentHashMap<>(); + + private MetricRegistry metrics; + private Object innerMock; + private MockStrongReference reference; + + /** + * Create tracker. + * + * @param type service type + * @param config config + * @param metrics metrics + */ + public Tracker(final Class type, + final TrackerConfig config, + final MetricRegistry metrics) { + this.type = type; + this.config = config; + this.warn = config.getSlowMethods() > 0 + ? Duration.of(config.getSlowMethods(), config.getSlowMethodsUnit()) : null; + this.metrics = metrics; + } + + /** + * @return type of tracked bean + */ + public Class getType() { + return type; + } + + /** + * @return true if no method calls registered, false otherwise + */ + public boolean isEmpty() { + return tracks.isEmpty(); + } + + /** + * @return count of tracked methods + */ + public int size() { + return tracks.size(); + } + + /** + * Tracks sorted by start time. + * + * @return all tracked method calls + */ + public List getTracks() { + return new ArrayList<>(tracks); + } + + /** + * @return last tracked method call + * @throws java.lang.IllegalStateException if no calls tracked (error thrown to simplify usage - no additional + * checks required in test) + */ + public MethodTrack getLastTrack() { + Preconditions.checkState(!tracks.isEmpty(), "No tracks registered"); + return tracks.get(tracks.size() - 1); + } + + /** + * Returns last (count) tracks in execution order. + * + * @param count last tracks count + * @return last tracks (count) + * @throws java.lang.IllegalStateException if there is not enough recorded tracks + */ + public List getLastTracks(final int count) { + Preconditions.checkState(tracks.size() >= count, + "Not enough tracks registered: requested %s but only %s registered", count, tracks.size()); + return tracks.subList(tracks.size() - count, tracks.size()); + } + + /** + * NOTE: This is just an example usage - you can create a stats object with filtered methods + * (or with methods from multiple trackers). + * + * @return stats for all tracked methods (for reporting) + */ + public TrackerStats getStats() { + return new TrackerStats(tracks); + } + + /** + * Tracked methods filtering using mockito. Useful because mockito provides type-safe syntax. For example, + * search by method: {@code find(mock -> when(mock.something())}. Search by method with exact argument == 1: + * {@code find(mock -> when(mock.something(intThat(argument -> argument == 1)))}. + * + * @param where search condition definition + * @return list of matched tracks + */ + // not just find for groovy support (find is a default groovy method) + public List findTracks(final Function> where) { + final OngoingStubbing apply = where.apply(mock()); + final InvocationContainerImpl invocationContainer = MockUtil.getInvocationContainer(mock()); + try { + // it is a hack, but it's the only way to access matcher + final MatchableInvocation matcher = (MatchableInvocation) FieldUtils + .readDeclaredField(invocationContainer, "invocationForStubbing", true); + + final List result = new ArrayList<>(); + for (final MethodTrack track : tracks) { + if (matcher.matches(from(track))) { + result.add(track); + } + } + return result; + + } catch (Exception ex) { + throw new IllegalStateException("Failed to find tracker method", ex); + } finally { + // to finish stubbing config and avoid errors + apply.thenReturn(null); + MockUtil.resetMock(mock()); + } + } + + /** + * Cleanup recorded tracks. By default, called automatically after each test method. + */ + public void clear() { + tracks.clear(); + timers.clear(); + metrics = new MetricRegistry(); + } + + @SuppressWarnings({"ParameterNumber", "PMD.UseVarargs", "PMD.SystemPrintln"}) + void add(final Method method, + final String instanceHash, + final long started, + final Duration duration, + final Object[] rawArguments, + final String[] arguments, + final Object rawResult, + final String result, + final Throwable throwable, + final boolean[] stringMarkers) { + final Timer timer = getTimer(method); + timer.update(duration); + final MethodTrack track = new MethodTrack(type, method, instanceHash, started, duration, + config.isKeepRawObjects() ? rawArguments : null, + arguments, + config.isKeepRawObjects() ? rawResult : null, + result, throwable, stringMarkers, timer); + synchronized (tracks) { + tracks.add(track); + // sort to order tracks according to START TIME and not by the end time, as they add here + // (important for getLastTracks feature) + Collections.sort(tracks); + } + if (config.isTrace() || (warn != null && duration.compareTo(warn) > 0)) { + final String msg = "\\\\\\---[Tracker<" + type.getSimpleName() + ">]" + + String.format(" %-12s <@%s> .%s", + PrintUtils.ms(track.getDuration()), instanceHash, track.toStringTrack()); + if (config.isTrace()) { + // logger is not used here because debug and trace would not be visible and warnings would confuse + System.out.println(msg); + } else { + logger.warn("\n" + msg); + } + } + } + + private Timer getTimer(final Method method) { + return timers.computeIfAbsent(method, + k -> metrics.timer(type.getName() + "." + method.getName() + "." + + Arrays.stream(method.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(",")))); + } + + /** + * @return mock to use for methods search ({@link #findTracks(java.util.function.Function)}) + */ + @SuppressWarnings("unchecked") + private T mock() { + // lazy mock creation (if used) + if (innerMock == null) { + innerMock = Mockito.mock(type); + reference = new MockStrongReference<>(innerMock, false); + } + return (T) innerMock; + } + + private InterceptedInvocation from(final MethodTrack track) { + return new InterceptedInvocation( + reference, + new DelegatingMethod(track.getMethod()), + track.getRawArguments(), + null, + LocationFactory.create(), + SequenceNumber.next()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerConfig.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerConfig.java new file mode 100644 index 000000000..96f3fa70f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerConfig.java @@ -0,0 +1,109 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import java.time.temporal.ChronoUnit; + +/** + * {@link ru.vyarus.dropwizard.guice.test.track.Tracker} configuration. + * + * @author Vyacheslav Rusakov + * @since 30.04.2025 + */ +public class TrackerConfig { + + private boolean trace; + private long slowMethods = 5; + private ChronoUnit slowMethodsUnit = ChronoUnit.SECONDS; + private boolean keepRawObjects = true; + private int maxStringLength = 30; + + /** + * When enabled, prints called method just after it's execution (with called arguments and returned result). + * Not enabled by default to avoid output mess in case when many methods would be called during test. + * + * @return true to print each method execution + */ + public boolean isTrace() { + return trace; + } + + /** + * @param trace true to print each method execution + */ + public void setTrace(final boolean trace) { + this.trace = trace; + } + + /** + * Print warnings about methods executing longer than the specified threshold. Set to 0 to disable warnings. + * + * @return slow method threshold (in seconds, by default - see {@link #getSlowMethodsUnit()}) + */ + public long getSlowMethods() { + return slowMethods; + } + + /** + * @param slowMethods slow method threshold (in seconds, by default - see {@link #getSlowMethodsUnit()}) + */ + public void setSlowMethods(final long slowMethods) { + this.slowMethods = slowMethods; + } + + /** + * Unit for {@link #getSlowMethods()} threshold value (seconds by default). + * + * @return unit for threshold value + */ + public ChronoUnit getSlowMethodsUnit() { + return slowMethodsUnit; + } + + /** + * @param slowMethodsUnit unit for threshold value + */ + public void setSlowMethodsUnit(final ChronoUnit slowMethodsUnit) { + this.slowMethodsUnit = slowMethodsUnit; + } + + /** + * It is more likely that trackers would be used mostly for "call and verify" scenarios where keeping raw + * arguments makes perfect sense. That's why it's enabled by default. + *

        + * Important: method arguments and the result objects state could be mutable and change after or during method + * execution (and so be irrelevant for tracks analysis). For such cases, the tracker always holds string + * representations of method arguments and the result (rendered in method execution time). + *

        + * It makes sense to disable option if too many method executions appear during the test (e.g., tracker used + * for performance metrics). + * + * @return true to keep raw arguments and result objects + */ + public boolean isKeepRawObjects() { + return keepRawObjects; + } + + /** + * @param keepRawObjects true to keep raw arguments and result objects + */ + public void setKeepRawObjects(final boolean keepRawObjects) { + this.keepRawObjects = keepRawObjects; + } + + /** + * Required to keep called method toString rendering readable in case of large strings used. + * Note that for non-string objects, an object type with identity hash would be shown (not rely on toString + * because it would be too much unpredictable). + * + * @return maximum length of string in method parameter or returned result + */ + public int getMaxStringLength() { + return maxStringLength; + } + + /** + * @param maxStringLength maximum length of string in method parameter or returned result + */ + public void setMaxStringLength(final int maxStringLength) { + this.maxStringLength = maxStringLength; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerProxy.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerProxy.java new file mode 100644 index 000000000..844c36f43 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackerProxy.java @@ -0,0 +1,76 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Stopwatch; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +/** + * AOP interceptor redirect calls from the real bean into spy object, which was created around the same real bean. + *

        + * There is a chicken-egg problem: service binding can't be overridden (with spy instance), because spy requires + * service instance for construction. So, instead of replacing bean, we intercept bean calls. Actual spy object + * is created lazily just after injector creation. On the first call, AOP interceptor breaks the current aop chain + * (if other interceptors registered) and redirect calls to spy, which again calls the same service (including + * aop handler), but, this time, it processes normally. + * + * @param bean type + */ +public class TrackerProxy implements MethodInterceptor { + private final Tracker tracker; + private final int maxToString; + + /** + * Create proxy. + * + * @param type service type + * @param config config + * @param metrics metrics + */ + public TrackerProxy(final Class type, final TrackerConfig config, final MetricRegistry metrics) { + this.maxToString = config.getMaxStringLength(); + this.tracker = new Tracker<>(type, config, metrics); + } + + /** + * @return tracker instance + */ + public Tracker getTracker() { + return tracker; + } + + @Override + public synchronized Object invoke(final MethodInvocation methodInvocation) throws Throwable { + final long start = System.currentTimeMillis(); + final Stopwatch timer = Stopwatch.createStarted(); + // objects might be changed after or during method execution + final Object[] arguments = methodInvocation.getArguments(); + final String[] args = PrintUtils.toStringArguments(arguments, maxToString); + final boolean[] stringMarkers = new boolean[args.length + 1]; + for (int i = 0; i < args.length; i++) { + stringMarkers[i] = arguments[i] instanceof String; + } + Object result = null; + Throwable error = null; + try { + result = methodInvocation.proceed(); + return result; + } catch (Throwable e) { + error = e; + throw e; + } finally { + stringMarkers[stringMarkers.length - 1] = result instanceof String; + tracker.add(methodInvocation.getMethod(), + PrintUtils.identity(methodInvocation.getThis()), + start, + timer.elapsed(), + arguments, + args, + result, + error != null ? null : PrintUtils.toStringValue(result, maxToString), + error, + stringMarkers); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackersHook.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackersHook.java new file mode 100644 index 000000000..fb054adb4 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/TrackersHook.java @@ -0,0 +1,197 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Preconditions; +import com.google.inject.matcher.Matchers; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +/** + * Track method calls on any guice bean and records arguments and return values, together with measuring time. + *

        + * Useful for validating called method arguments and return value (when service called indirectly - by another top-level + * service). In this sense it is very close to mockito spy + * ({@link ru.vyarus.dropwizard.guice.test.spy.SpiesHook}), but api is simpler. + * Tracker collects both raw in/out objects and string (snapshot) version (because mutable objects could change). + * Raw objects holding could be disabled ({@link TrackersHook.Builder#keepRawObjects(boolean)}). + *

        + * Another use-case is slow methods detection: tracker counts each method execution time, and after test could + * print a report indicating the slowest methods. Or it could be used to simply print all called methods to console + * with {@link TrackersHook.Builder#trace(boolean)} (could be useful during behavior investigations). Another option + * is to configure a slow method threshold: then only methods above a threshold would be logged with WARN. + *

        + * Example usage: + *

        
        + *     TrackersHook hook = new TrackersHook()
        + *     Tracker<Service> tracker = hook.track(Service.class)
        + *                                          // optional configuration
        + *                                          .add()
        + *     // after service methods execution
        + *     tracker.getTracks()
        + * 
        + *

        + * Tracking is implemented with a custom AOP handler which intercepts all bean calls and record them. + * Can be used together with mocks, spies or stubs + *

        + * Limitation: could track only beans, created by guice (due to used AOP). Does not work for HK2 beans. + * + * @author Vyacheslav Rusakov + * @since 28.04.2025 + */ +public class TrackersHook implements GuiceyConfigurationHook { + + // independent from dropwizard app registry + private final MetricRegistry metrics = new MetricRegistry(); + private final Map, TrackerProxy> trackers = new HashMap<>(); + private boolean initialized; + + @Override + public void configure(final GuiceBundle.Builder builder) throws Exception { + if (!trackers.isEmpty()) { + builder.modulesOverride(binder -> { + trackers.forEach((type, proxy) -> { + // real binding isn't overridden, just used aop to intercept all calls + binder.bindInterceptor(Matchers.only(type), Matchers.any(), proxy); + }); + }); + } + initialized = true; + } + + /** + * Start bean tracker registration. + * + * @param type bean type + * @param bean type + * @return builder to configure tracker + * @throws java.lang.IllegalStateException if tracker for bean already registered + */ + public Builder track(final Class type) { + Preconditions.checkState(!initialized, "Too late. Trackers already applied."); + Preconditions.checkState(!trackers.containsKey(type), "Tracker object for type %s is already registered.", + type.getSimpleName()); + return new Builder<>(type); + } + + /** + * @param type bean type + * @param bean type + * @return bean tracker instance + * @throws java.lang.IllegalStateException if tracker for bean is not registered + */ + @SuppressWarnings("unchecked") + public Tracker getTracker(final Class type) { + return (Tracker) Preconditions.checkNotNull(trackers.get(type), + "Tracker not registered for type %s", type.getSimpleName()).getTracker(); + } + + /** + * Clear recorded data for all trackers. + */ + public void resetTrackers() { + trackers.values().forEach(proxy -> proxy.getTracker().clear()); + } + + /** + * Tracker configuration builder. + * + * @param bean type + */ + public class Builder { + private final Class type; + private final TrackerConfig config = new TrackerConfig(); + + /** + * Create builder. + * + * @param type service type + */ + public Builder(final Class type) { + this.type = type; + } + + /** + * When enabled, prints called method just after it's execution (with called arguments and returned result). + * Not enabled by default to avoid output mess in case when many methods would be called during test. + * + * @param trace true to print each method execution + * @return builder instance for chained calls + */ + public Builder trace(final boolean trace) { + config.setTrace(trace); + return this; + } + + /** + * Print warnings about methods executing longer than the specified threshold. + * + * @param maxTime slow method threshold + * @param unit threshold unit + * @return builder instance for chained calls + * @see #disableSlowMethodsLogging() for disabling slow time logging + */ + public Builder slowMethods(final long maxTime, final ChronoUnit unit) { + config.setSlowMethods(maxTime); + config.setSlowMethodsUnit(unit); + return this; + } + + /** + * Disable slow methods warning (by default, showing methods executed longer than 5 seconds). + * + * @return builder instance for chained calls + * @see #slowMethods(long, java.time.temporal.ChronoUnit) for changing the default threshold + */ + public Builder disableSlowMethodsLogging() { + config.setSlowMethods(0); + return this; + } + + /** + * It is more likely that trackers would be used mostly for "call and verify" scenarios where keeping raw + * arguments makes perfect sense. That's why it's enabled by default. + *

        + * Important: method arguments and the result objects state could be mutable and change after or during method + * execution (and so be irrelevant for tracks analysis). For such cases, the tracker always holds string + * representations of method arguments and the result (rendered in method execution time). + *

        + * It makes sense to disable option if too many method executions appear during the test (e.g., tracker used + * for performance metrics). + * + * @param keepRawObjects true to keep raw arguments and result objects + * @return builder instance for chained calls + */ + public Builder keepRawObjects(final boolean keepRawObjects) { + config.setKeepRawObjects(keepRawObjects); + return this; + } + + /** + * Required to keep called method toString rendering readable in case of large strings used. + * Note that for non-string objects, an object type with identity hash would be shown (not rely on toString + * because it would be too much unpredictable). + * + * @param maxStringLength maximum length of string in method parameter or returned result + * @return builder instance for chained calls + */ + public Builder maxStringLength(final int maxStringLength) { + config.setMaxStringLength(maxStringLength); + return this; + } + + /** + * Apply tracker registration. Returned object should be used to access recorded tracks. + * + * @return configured and registered tracker + */ + public Tracker add() { + final TrackerProxy proxy = new TrackerProxy<>(type, config, metrics); + trackers.put(type, proxy); + return proxy.getTracker(); + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/MethodSummary.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/MethodSummary.java new file mode 100644 index 000000000..72236c219 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/MethodSummary.java @@ -0,0 +1,160 @@ +package ru.vyarus.dropwizard.guice.test.track.stat; + +import com.codahale.metrics.Snapshot; +import com.codahale.metrics.Timer; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Tracked method summary (for all calls of exact method). + * + * @author Vyacheslav Rusakov + * @since 12.02.2025 + */ +@SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") +public class MethodSummary implements Comparable { + + private final Class service; + private final Method method; + private final Set instances = new HashSet<>(); + + private int tracks; + private int errors; + private final Snapshot snapshot; + + /** + * Create summary. + * + * @param service service type + * @param method method + * @param timer method metrics + */ + public MethodSummary(final Class service, final Method method, final Timer timer) { + this.service = service; + this.method = method; + this.snapshot = timer.getSnapshot(); + } + + /** + * Used during summary object aggregation. + * + * @param track track to append to summary + */ + public void add(final MethodTrack track) { + tracks++; + if (!track.isSuccess()) { + errors++; + } + instances.add(track.getInstanceHash()); + } + + /** + * @return method bean class + */ + public Class getService() { + return service; + } + + /** + * @return method itself + */ + public Method getMethod() { + return method; + } + + /** + * @return registered tracks count + */ + public int getTracks() { + return tracks; + } + + /** + * @return failed calls count + */ + public int getErrors() { + return errors; + } + + /** + * @return metrics timer snapshot (for reporting) + */ + public Snapshot getMetrics() { + return snapshot; + } + + /** + * @return minimal method call duration (string representation) + */ + public String getMin() { + return PrintUtils.formatMetric(snapshot.getMin()); + } + + /** + * @return maximum method call duration (string representation) + */ + public String getMax() { + return PrintUtils.formatMetric(snapshot.getMax()); + } + + /** + * @return median method call duration (string representation) + */ + public String getMedian() { + return PrintUtils.formatMetric(snapshot.getMedian()); + } + + /** + * In test, the first execution would be slow (jvm warm up). This percentile should show more or less + * correct (hot) method execution time, in case of many executions (useful for "raw performance" checks). + * + * @return duration of 75% method calls (string representation) + */ + public String get75thPercentile() { + return PrintUtils.formatMetric(snapshot.get75thPercentile()); + } + + /** + * @return duration of 95% method calls (string representation) + */ + public String get95thPercentile() { + return PrintUtils.formatMetric(snapshot.get95thPercentile()); + } + + /** + * @return number of different bean instances used for method calls + */ + public int getInstancesCount() { + return instances.size(); + } + + /** + * Method used for console reporting. Return type is not included because the method could be identified by + * arguments only. + * + * @return string representation for method (with argument types, but without return type) + */ + public String toStringMethod() { + return method.getName() + "(" + + Arrays.stream(method.getParameterTypes()).map(Class::getSimpleName).collect(Collectors.joining(", ")) + + ")"; + } + + @Override + public String toString() { + return toStringMethod() + " called " + tracks + " times" + (errors > 0 ? " (" + errors + ")" : "") + + (instances.size() > 1 ? " (on " + instances.size() + " instances)" : ""); + } + + @Override + public int compareTo(final MethodSummary o) { + return Double.compare(snapshot.getMedian(), o.snapshot.getMedian()); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/TrackerStats.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/TrackerStats.java new file mode 100644 index 000000000..5031e5753 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/track/stat/TrackerStats.java @@ -0,0 +1,94 @@ +package ru.vyarus.dropwizard.guice.test.track.stat; + +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Default tracker report implementation: shows all tracked methods, sorted by duration (the slowest go first). + * + * @author Vyacheslav Rusakov + * @since 12.02.2025 + */ +public class TrackerStats { + + private final List methods; + + /** + * Create stats. + * + * @param trackers trackers + */ + @SuppressWarnings({"unchecked", "PMD.UnnecessaryCast"}) + public TrackerStats(final Tracker... trackers) { + this((List) Arrays.stream(trackers) + .map(Tracker::getTracks) + .flatMap(List::stream) + .collect(Collectors.toList())); + } + + /** + * Create stats. + * + * @param tracks tracks + */ + public TrackerStats(final List tracks) { + methods = buildSummary(tracks); + } + + /** + * @return methods summary sorted by the slowness (the slowest go first) + */ + public List getMethods() { + return methods; + } + + /** + * @return rendered report or null if no tracks + */ + public String render() { + if (methods.isEmpty()) { + return null; + } + final String format = "%-40s %-50s %-10s %-10s %-10s %-10s %-10s %-10s %-10s%n"; + final StringBuilder builder = new StringBuilder(); + builder.append('\t').append(String.format(format, + "[service]", "[method]", "[calls]", "[fails]", "[min]", "[max]", "[median]", "[75%]", "[95%]")); + for (final MethodSummary summary : methods) { + builder.append('\t').append(String.format(format, + summary.getService().getSimpleName(), + summary.toStringMethod(), + summary.getTracks() + + (summary.getInstancesCount() > 1 ? " (" + summary.getInstancesCount() + ")" : ""), + summary.getErrors(), + summary.getMin(), + summary.getMax(), + summary.getMedian(), + summary.get75thPercentile(), + summary.get95thPercentile())); + } + return builder.toString(); + } + + private List buildSummary(final List tracks) { + final Map idx = new HashMap<>(); + for (final MethodTrack track : tracks) { + idx.computeIfAbsent(track.getMethod(), + method -> new MethodSummary(track.getService(), track.getMethod(), track.getTimer())) + .add(track); + } + final List res = new ArrayList<>(idx.values()); + Collections.sort(res); + // top slow above + Collections.reverse(res); + return res; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ClassFilters.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ClassFilters.java new file mode 100644 index 000000000..e47e3a430 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ClassFilters.java @@ -0,0 +1,74 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.function.Predicate; + +/** + * Simple class {@link java.util.function.Predicate} implementations. + * Supposed to be used with + * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#autoConfigFilter(java.util.function.Predicate)}. + * + * @author Vyacheslav Rusakov + * @since 28.03.2025 + */ +public final class ClassFilters { + + private ClassFilters() { + } + + /** + * @param annotation annotations to check + * @return predicate accepting classes, annotated with (at least) one of the provided annotations. + */ + @SafeVarargs + public static Predicate> annotated(final Class... annotation) { + return type -> Arrays.stream(annotation) + .anyMatch(type::isAnnotationPresent); + } + + /** + * @param packages packages to accept + * @return predicate accepting classes in provided packages + */ + public static Predicate> inPackages(final String... packages) { + return type -> Arrays.stream(packages) + .anyMatch(pkg -> type.getName().startsWith(pkg)); + } + + /** + * This might be useful for tests to limit classpath scan by test class only. + * + * @param baseClasses base (declaration) classes + * @return predicate accepting classes declared in one of the provided base classes + */ + public static Predicate> declaredIn(final Class... baseClasses) { + return type -> type.getDeclaringClass() != null && Arrays.stream(baseClasses) + .anyMatch(base -> type.getDeclaringClass().equals(base)); + } + + /** + * @param annotation annotations to check + * @return predicate accepting classes not annotated with any of the provided annotations + */ + @SafeVarargs + public static Predicate> ignoreAnnotated(final Class... annotation) { + return annotated(annotation).negate(); + } + + /** + * @param packages packages to ignore + * @return predicate accepting classes not from the declared packages + */ + public static Predicate> ignorePackages(final String... packages) { + return inPackages(packages).negate(); + } + + /** + * @param baseClasses base (declaration) classes + * @return predicate accepting classes not declared in one of the provided base classes + */ + public static Predicate> ignoreDeclaredIn(final Class... baseClasses) { + return declaredIn(baseClasses).negate(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifier.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifier.java new file mode 100644 index 000000000..54a6df870 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifier.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import io.dropwizard.core.Configuration; + +/** + * Configuration modifier is an alternative for configuration override, which is limited for simple + * property types (for example, a collection could not be overridden). + *

        + * Modifier is called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml and instance-based configurations. + *

        + * Could be declared in: + * - {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension.Builder#configModifiers( + * ConfigModifier[])} extension builder (same for dropwizard extension) + * - {@link ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp#configModifiers()} extension annotation (same for + * dropwizard annotation) + * - {@link ru.vyarus.dropwizard.guice.test.TestSupport#build(Class)} - generic test support object builder + * - {@link ru.vyarus.dropwizard.guice.test.TestSupport#buildCommandRunner(Class)} - generic test command runner + * - {@link ru.vyarus.dropwizard.guice.test.GuiceyTestSupport#configModifiers(ConfigModifier[])} - guicey support + * object (directly) + * - {@link ru.vyarus.dropwizard.guice.test.cmd.CommandTestSupport#configModifiers(ConfigModifier[])} - command + * support object (directly) + *

        + * To enable support for {@link io.dropwizard.testing.DropwizardTestSupport} custom command factory must be used: + * {@link ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils#buildCommandFactory(java.util.List)}. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 04.03.2025 + */ +@FunctionalInterface +public interface ConfigModifier { + + /** + * Called before application run phase. Only logger configuration is applied at this moment (and so you + * can't change it). Modifier would work with both yaml and instance-based configurations. + * + * @param config configuration instance + * @throws Exception on error (to avoid try-catch blocks in modifier itself) + */ + void modify(C config) throws Exception; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifierServerCommand.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifierServerCommand.java new file mode 100644 index 000000000..8f787d77f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigModifierServerCommand.java @@ -0,0 +1,41 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.ServerCommand; +import io.dropwizard.core.setup.Bootstrap; +import net.sourceforge.argparse4j.inf.Namespace; + +import java.util.List; + +/** + * Dropwizard {@link io.dropwizard.core.cli.ServerCommand} with configuration modifiers support. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 04.03.2025 + */ +public class ConfigModifierServerCommand extends ServerCommand { + + private final List> modifiers; + + /** + * Create a command. + * + * @param application application instance + * @param modifiers modifiers + */ + public ConfigModifierServerCommand(final Application application, final List> modifiers) { + super(application); + this.modifiers = modifiers; + } + + @Override + protected void run(final Bootstrap bootstrap, + final Namespace namespace, + final C configuration) throws Exception { + // at this point only logging configuration performed + ConfigOverrideUtils.runModifiers(configuration, modifiers); + super.run(bootstrap, namespace, configuration); + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java similarity index 94% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java index c94951910..849e03884 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideExtensionValue.java @@ -28,6 +28,13 @@ public class ConfigOverrideExtensionValue extends ConfigOverride implements Conf private String originalValue; private String value; + /** + * Create a config override value. + * + * @param namespace namespace + * @param storageKey storage key + * @param configPath config yaml path + */ public ConfigOverrideExtensionValue(final ExtensionContext.Namespace namespace, final String storageKey, final String configPath) { diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java similarity index 58% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java index aa173cc03..0c2a7be3e 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideUtils.java @@ -1,13 +1,22 @@ package ru.vyarus.dropwizard.guice.test.util; import com.google.common.base.Preconditions; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.cli.ServerCommand; import io.dropwizard.testing.ConfigOverride; +import jakarta.annotation.Nullable; import org.junit.jupiter.api.extension.ExtensionContext; import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Function; /** * Config override handling utils. @@ -17,6 +26,8 @@ */ public final class ConfigOverrideUtils { + private static final String STAR = "*"; + private ConfigOverrideUtils() { } @@ -54,7 +65,7 @@ public static ConfigOverride[] convert(final String prefix, final String... prop int i = 0; for (String value : props) { final int idx = value.indexOf(':'); - Preconditions.checkState(idx > 0 && idx < value.length(), + Preconditions.checkState(idx > 0, "Incorrect configuration override declaration: must be 'key: value', but found '%s'", value); overrides[i++] = ConfigOverride .config(prefix, value.substring(0, idx).trim(), value.substring(idx + 1).trim()); @@ -145,4 +156,89 @@ public static ConfigOverride[] prepareExtensionOverrides(final ConfigOverride[] } return overrides; } + + /** + * Applies "/*" if not already specified in rest mapping. + * + * @param restMapping user-declared rest mapping string + * @return formatted rest mapping (for using in configuration) + */ + public static String formatRestMapping(final String restMapping) { + String mapping = PathUtils.leadingSlash(restMapping); + if (!mapping.endsWith(STAR)) { + mapping = PathUtils.trailingSlash(mapping) + STAR; + } + return mapping; + } + + /** + * Creates config override for rest mapping. Declared mapping if automatically "fixed" to comply with required + * format. + * + * @param prefix configuration override prefixes (may be null to use default prefix) + * @param restMapping rest mapping to configure + * @return config override object + */ + public static ConfigOverride overrideRestMapping(final @Nullable String prefix, final String restMapping) { + return ConfigOverride.config(prefix == null ? "dw." : prefix, + "server.rootPath", formatRestMapping(restMapping)); + } + + /** + * Instantiates provided configuration modifiers. + * + * @param modifiers configuration modifiers to instantiate + * @param configuration type + * @return hooks instances + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public static List> createModifiers( + final Class>... modifiers) { + final List> res = new ArrayList<>(); + for (Class> modifier : modifiers) { + try { + res.add((ConfigModifier) InstanceUtils.create(modifier)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate configuration modifier: " + + modifier.getSimpleName(), e); + } + } + return res; + } + + /** + * Runs configuration modifiers. + * + * @param configuration configuration instance + * @param modifiers configuration modifiers + * @param configuration type + * @throws java.lang.IllegalStateException if modifier fails to execute + */ + @SuppressWarnings("unchecked") + public static void runModifiers( + final C configuration, final List> modifiers) { + for (final ConfigModifier modifier : modifiers) { + try { + modifier.modify(configuration); + } catch (Exception e) { + throw new IllegalStateException("Configuration modification failed for " + + modifier.getClass().getName(), e); + } + } + } + + /** + * Create server command function with configuration modifiers support. + * + * @param modifiers configuration modifiers + * @param configuration type (required to align input) + * @return server command function with configuration modifiers support. + */ + public static Function, Command> buildCommandFactory( + final List> modifiers) { + return modifiers.isEmpty() + ? ServerCommand::new + : application -> new ConfigModifierServerCommand<>(application, modifiers); + } } diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java similarity index 93% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java index 5e93ea7e1..b339ec5d0 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigOverrideValue.java @@ -23,6 +23,12 @@ public class ConfigOverrideValue extends ConfigOverride implements ConfigurableP private String prefix; private String originalValue; + /** + * Create a config override value. + * + * @param key config yaml path + * @param value value + */ public ConfigOverrideValue(final String key, final Supplier value) { this.key = Preconditions.checkNotNull(key, "Property name required"); this.value = Preconditions.checkNotNull(value, "Value supplier required"); diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigurablePrefix.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigurablePrefix.java similarity index 100% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigurablePrefix.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ConfigurablePrefix.java diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java new file mode 100644 index 000000000..bb8965e57 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java @@ -0,0 +1,53 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Guicey {@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook} test utilities. + * + * @author Vyacheslav Rusakov + * @since 02.05.2020 + */ +public final class HooksUtil { + + private HooksUtil() { + } + + /** + * Instantiates provided hooks. + * + * @param hooks hooks to instantiate + * @return hooks instances + */ + @SafeVarargs + public static List create(final Class... hooks) { + final List res = new ArrayList<>(); + for (Class hook : hooks) { + try { + res.add(InstanceUtils.create(hook)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate guicey hook: " + hook.getSimpleName(), e); + } + } + return res; + } + + /** + * Register configuration hooks. + * + * @param hooks hooks to register + */ + public static void register(final List hooks) { + if (hooks != null) { + for (GuiceyConfigurationHook hook : hooks) { + if (hook != null) { + hook.register(); + } + } + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/PrintUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/PrintUtils.java new file mode 100644 index 000000000..beef89daa --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/PrintUtils.java @@ -0,0 +1,248 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +/** + * Utilities for test console reports formatting. + * + * @author Vyacheslav Rusakov + * @since 12.02.2025 + */ +@SuppressWarnings("PMD.GodClass") +public final class PrintUtils { + + private static final String DURATION_FORMATION = "%2.3f %s"; + private static final String VALUE_FORMATION = "%2.3f"; + + private PrintUtils() { + } + + /** + * This is the same string as shown in the default Object.toString (@hash part). + * + * @param object object to get identity for + * @return object identity string + */ + public static String identity(final Object object) { + return Integer.toHexString(System.identityHashCode(object)); + } + + /** + * Converts arguments to string. + * + * @param args arguments + * @param maxLength maximum string length to keep (longer strings get truncated) + * @return string representation of incoming arguments + */ + public static String[] toStringArguments(final Object[] args, final int maxLength) { + final String[] arguments = new String[args.length]; + for (int i = 0; i < args.length; i++) { + arguments[i] = toStringValue(args[i], maxLength); + } + return arguments; + } + + /** + * Converts any value (object) to string. Rules: + * - Primitive values, number and booleans stored as is + * - String values could be truncated + * - Objects represented as ObjectType@instanceHash + * - null is "null" + * + * @param value value to convert to string + * @param maxLength maximum string length to keep + * @return string representation for the object + */ + public static String toStringValue(final Object value, final int maxLength) { + if (value == null) { + return "null"; + } + String res; + if (value instanceof String) { + res = (String) value; + if (res.length() > maxLength) { + res = res.substring(0, maxLength) + "..."; + } + } else if (value.getClass().isPrimitive() || value instanceof Number || value instanceof Boolean) { + res = value.toString(); + } else if (value instanceof Collection) { + res = toStringCollection((Collection) value, maxLength); + } else if (value.getClass().isArray()) { + res = toStringArray((Object[]) value, maxLength); + } else { + res = value.getClass().getSimpleName() + "@" + identity(value); + } + return res; + } + + /** + * Render duration together with increase indication: overall time (increase since last report). + * + * @param overall overall duration (including increase) (could be null) + * @param increase increase since last report (could be null) + * @return string representation for suration + */ + public static String renderTime(final Duration overall, final Duration increase) { + return overall == null ? "--" : (ms(overall) + + (increase != null && increase.toNanos() > 0 ? (" ( + " + ms(increase) + ")") : "")); + } + + /** + * Render duration in milliseconds (2 decimal signs precision). + * + * @param duration duration + * @return string representation + */ + public static String ms(final Duration duration) { + return ms(duration.toNanos()); + } + + /** + * @param nanos time in nanoseconds + * @return string representation + */ + public static String ms(final long nanos) { + final long millis = Math.round((double) nanos / 1_000_000); + final String res; + if (millis > 10) { + res = millis + " ms"; + } else if (nanos < 10) { + res = "0.00 ms"; + } else if (nanos < 100) { + // bigger precision for too small numbers (to avoid confusion by showing raw nanos) + res = formatMs(nanos, 5); + } else if (nanos < 1000) { + res = formatMs(nanos, 4); + } else if (nanos < 10_000) { + res = formatMs(nanos, 3); + } else { + res = formatMs(nanos, 2); + } + return res; + } + + /** + * Format nano value in milliseconds with required precision (decimal numbers). + * + * @param nanos value in nanoseconds + * @param precision required precision (decimal numbers) + * @return formatted value + */ + public static String formatMs(final long nanos, final int precision) { + final String format = "%." + precision + "f ms"; + return String.format(format, new BigDecimal(nanos) + .divide(BigDecimal.valueOf(1_000_000), precision, RoundingMode.HALF_UP) + .doubleValue()); + } + + /** + * Time assumed to come from metrics snapshot. + * + * @param time time to format + * @return string representation (in ms) + */ + public static String formatMetric(final double time) { + return formatMetric(time, TimeUnit.MILLISECONDS); + } + + /** + * Formats time, obtained from metrics snapshot to target unit. + * + * @param time time to format + * @param unit target unit + * @return string representation (in selected unit) + */ + public static String formatMetric(final double time, final TimeUnit unit) { + if (unit == null) { + return String.format(VALUE_FORMATION, time); + } else { + return String.format(DURATION_FORMATION, convertDuration(time, unit), toStringUnit(unit)); + } + } + + /** + * Universal header string for performance reports in test to clearly identify current context. + * + * @param context junit context + * @return performance report header + */ + public static String getPerformanceReportSeparator(final ExtensionContext context) { + String inst = "---------------------------------\n"; + if (context.getTestInstance().isPresent()) { + inst = "/ test instance = " + identity(context.getTestInstance().get()) + " /\n"; + } + return "\n\\\\\\------------------------------------------------------------" + inst; + } + + private static String toStringCollection(final Collection list, final int maxLength) { + final StringBuilder builder = new StringBuilder("(").append(list.size()).append(")["); + if (!list.isEmpty()) { + builder.append(' '); + } + final Iterator it = list.iterator(); + int cnt = 0; + // show up to 10 values + while (it.hasNext() && cnt < 10) { + builder.append(cnt > 0 ? "," : "").append(toStringValue(it.next(), maxLength)); + cnt++; + } + if (list.size() > 10) { + builder.append(",..."); + } + if (!list.isEmpty()) { + builder.append(' '); + } + + return builder.append(']').toString(); + } + + private static String toStringArray(final Object[] array, final int maxLength) { + return toStringCollection(Arrays.asList(array), maxLength); + } + + + @SuppressWarnings("PMD.ExhaustiveSwitchHasDefault") + private static String toStringUnit(final TimeUnit unit) { + final String res; + switch (unit) { + case NANOSECONDS: + res = "ns"; + break; + case MICROSECONDS: + res = "μs"; + break; + case MILLISECONDS: + res = "ms"; + break; + case SECONDS: + res = "s"; + break; + case MINUTES: + res = "m"; + break; + case HOURS: + res = "h"; + break; + case DAYS: + res = "d"; + break; + default: + res = unit.toString().toLowerCase(); + break; + } + return res; + } + + private static double convertDuration(final double duration, final TimeUnit durationUnit) { + final double durationFactor = 1.0 / durationUnit.toNanos(1); + return duration * durationFactor; + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java similarity index 60% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java index 86d2a627d..57811c0e5 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RandomPortsListener.java @@ -1,21 +1,23 @@ package ru.vyarus.dropwizard.guice.test.util; -import io.dropwizard.Configuration; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.server.DefaultServerFactory; +import io.dropwizard.core.server.ServerFactory; +import io.dropwizard.core.server.SimpleServerFactory; +import io.dropwizard.core.setup.Environment; import io.dropwizard.jetty.HttpConnectorFactory; -import io.dropwizard.server.DefaultServerFactory; -import io.dropwizard.server.ServerFactory; -import io.dropwizard.server.SimpleServerFactory; -import io.dropwizard.setup.Environment; import io.dropwizard.testing.DropwizardTestSupport; /** * Applies random ports to test application. + * + * @param configuration type */ -public class RandomPortsListener extends DropwizardTestSupport.ServiceListener { +public class RandomPortsListener extends DropwizardTestSupport.ServiceListener { @Override - public void onRun(final Configuration configuration, + public void onRun(final C configuration, final Environment environment, - final DropwizardTestSupport rule) throws Exception { + final DropwizardTestSupport rule) throws Exception { final ServerFactory server = configuration.getServerFactory(); if (server instanceof SimpleServerFactory) { ((HttpConnectorFactory) ((SimpleServerFactory) server).getConnector()).setPort(0); diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ReusableAppUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ReusableAppUtils.java new file mode 100644 index 000000000..51e09742b --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/ReusableAppUtils.java @@ -0,0 +1,203 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.commons.util.ReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +/** + * Reusable application support for tests. Reusable application must be declared in base test class (preferably + * abstract) to guarantee that all derived tests would use THE SAME extension declaration. + *

        + * Reusable application stored in root context under spacial key: base test class name. DW_SUPPORT key can't be used + * because root storage is visible by all tests, but only some of them could extend base class and so should + * actually use global application. Moreover, there might be multiple reusable applications used (when multiple + * base classes used with reusable application declaration). + *

        + * Global application would be started by the first test and shut down after all tests execution (automatically + * on storage close). + *

        + * There is an additional api for extensions to be able to close reusable application: + * {@link ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport#closeReusableApp( + * org.junit.jupiter.api.extension.ExtensionContext)}. + * + * @author Vyacheslav Rusakov + * @since 19.12.2022 + */ +public final class ReusableAppUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(ReusableAppUtils.class); + + private ReusableAppUtils() { + } + + /** + * Search for exact class in test class clas hierarchy where annotation was declared. + * + * @param test test class + * @param ann annotation to find + * @return found annotation + * @throws java.lang.NullPointerException if annotation not found + * @throws java.lang.IllegalStateException if declaration is not correct + */ + public static Class findDeclarationClass(final Class test, final Annotation ann) { + Class sup = test; + final Class annType = ann.annotationType(); + Class decl = null; + while (decl == null && sup != null && !Object.class.equals(sup)) { + final Annotation cand = sup.getDeclaredAnnotation(annType); + if (cand != null && cand.equals(ann)) { + decl = sup; + } + sup = sup.getSuperclass(); + } + Preconditions.checkNotNull(decl, "Failed to find declaration class for @%s in test class %s hierarchy", + ann.annotationType().getSimpleName(), test.getName()); + validateDeclaringClass(test, decl, test.getName() + " (@" + annType.getSimpleName() + ")"); + return decl; + } + + /** + * Search for field in test class hierarchy where extension was declared. + *

        + * Field would not be found if extension declared in non-static field. + * + * @param test test class + * @param ext extension instance + * @return field declared extension + * @throws java.lang.NullPointerException if field not found + * @throws java.lang.IllegalStateException if declaration is not correct + */ + public static Field findExtensionField(final Class test, final GuiceyExtensionsSupport ext) { + final List fields = ReflectionUtils.findFields(test, + field -> Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(RegisterExtension.class), + ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP); + Field target = null; + for (Field field : fields) { + try { + final Object value = ReflectionUtils.tryToReadFieldValue(field).get(); + if (value != null && value.equals(ext)) { + target = field; + break; + } + } catch (Exception ex) { + throw new IllegalStateException("Failed to read field value: " + field.getName(), ex); + } + } + Preconditions.checkNotNull(target, "Failed to find declaration field for %s extension in test " + + "class %s hierarchy. Probably, reusable app declared in non-static field.", + ext.getClass().getSimpleName(), test.getName()); + final Class declaringClass = target.getDeclaringClass(); + validateDeclaringClass(test, declaringClass, declaringClass.getName() + "." + target.getName()); + return target; + } + + /** + * Register reusable app declaration source. + * + * @param test test class + * @param ext extension instance + * @param config extension config + */ + public static void registerField(final Class test, + final GuiceyExtensionsSupport ext, + final ExtensionConfig config) { + final Field field = findExtensionField(test, ext); + config.reuseDeclarationClass = field.getDeclaringClass(); + config.reuseSource = field.getDeclaringClass().getName() + "." + field.getName(); + } + + /** + * Register reusable app declaration source. + * + * @param test test class + * @param ann extension annotation + * @param config extension config + */ + public static void registerAnnotation(final Class test, + final Annotation ann, + final ExtensionConfig config) { + final Class declare = findDeclarationClass(test, ann); + config.reuseDeclarationClass = declare; + config.reuseSource = declare.getName() + "@" + ann.annotationType().getSimpleName(); + } + + /** + * @param context any context + * @return global store where reusable application must be stored + */ + public static ExtensionContext.Store getGlobalStore(final ExtensionContext context) { + return context.getRoot().getStore(ExtensionContext.Namespace.create(GuiceyExtensionsSupport.class)); + } + + /** + * @param context any context + * @param declarationClass base test class where reusable app was declared + * @return reusable app holder or null + */ + public static synchronized StoredReusableApp getGlobalApp( + final ExtensionContext context, final Class declarationClass) { + final ExtensionContext.Store global = getGlobalStore(context); + return (StoredReusableApp) global.get(getKey(declarationClass)); + } + + /** + * Stores new application in global context. + * + * @param context any context + * @param app application holder to store + */ + public static synchronized void registerGlobalApp(final ExtensionContext context, final StoredReusableApp app) { + final ExtensionContext.Store globalStore = getGlobalStore(context); + final String key = getKey(app.getDeclaration()); + // just in case + if (globalStore.get(key) != null) { + throw new IllegalStateException(String.format("Can't register reusable application %s because " + + "another application %s is already registered for base class %s", app.getSource(), + ((StoredReusableApp) globalStore.get(key)).getSource(), app.getDeclaration().getName())); + } + globalStore.put(key, app); + } + + /** + * Do nothing if reusable application not found for provided base class. + * + * @param context any context + * @param declarationClass base test class where reusable app was declared + * @return true if app was closed, false otherwise + */ + public static synchronized boolean closeGlobalApp(final ExtensionContext context, + final Class declarationClass) { + final StoredReusableApp app = getGlobalApp(context, declarationClass); + if (app != null) { + LOGGER.warn("Requested manual close for reusable app {}", app.getSource()); + try { + app.close(); + return true; + } catch (Exception e) { + LOGGER.error("Error closing reusable app manually", e); + } finally { + getGlobalStore(context).remove(getKey(declarationClass)); + } + } + return false; + } + + private static void validateDeclaringClass(final Class test, final Class declaration, final String source) { + Preconditions.checkState(!test.equals(declaration), + "Application declared in %s can't be reused because reusable declaration must be in abstract (base) " + + "class so tests could share the same declaration", source); + } + + private static String getKey(final Class declarationClass) { + return declarationClass.getName(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RunResult.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RunResult.java new file mode 100644 index 000000000..b77726314 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/RunResult.java @@ -0,0 +1,102 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import com.google.inject.Injector; +import com.google.inject.Key; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; + + +/** + * Application run result object for {@link ru.vyarus.dropwizard.guice.test.TestSupport} runs. It is important + * to construct this object in time of running application because it would be impossible to reference these + * objects after application shutdown. + *

        + * Object supposed to be used for assertions after the application stopped. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 24.11.2023 + */ +public class RunResult { + private final DropwizardTestSupport support; + private final Injector injector; + + /** + * Create result. + * + * @param support dropwizard test support + * @param injector injector + */ + public RunResult(final DropwizardTestSupport support, final Injector injector) { + this.support = support; + this.injector = injector; + } + + /** + * @return support object used for application run + */ + public DropwizardTestSupport getSupport() { + return support; + } + + /** + * @return injector, created during application run + */ + public Injector getInjector() { + return injector; + } + + /** + * @param application type + * @return application instance (used for run) + */ + public > T getApplication() { + return support.getApplication(); + } + + /** + * @return environment instance (used for run) + */ + public Environment getEnvironment() { + return support.getEnvironment(); + } + + /** + * @return configuration instance + */ + public C getConfiguration() { + return support.getConfiguration(); + } + + /** + * Access guice bean. + * + * @param type bean type + * @param target type + * @return bean instance or null + */ + public T getBean(final Class type) { + return getBean(Key.get(type)); + } + + /** + * Access guice bean by mapping key (for qualified or generified bindings). + * + * @param key bean key + * @param target type + * @return bean instance + */ + public T getBean(final Key key) { + return injector.getInstance(key); + } + + /** + * @return true for full web app run, false for core run (guice injector only) + */ + public boolean isWebRun() { + return !(support instanceof GuiceyTestSupport); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/StoredReusableApp.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/StoredReusableApp.java new file mode 100644 index 000000000..7e150294b --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/StoredReusableApp.java @@ -0,0 +1,80 @@ +package ru.vyarus.dropwizard.guice.test.util; + +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.vyarus.dropwizard.guice.test.ClientSupport; + +/** + * Storage value for reusable application. Value would be created by the first test (using reusable app) and + * would be closed after all tests (automatically by junit). + *

        + * Reusable app is identified in root storage by declaration class name because only one application extension + * could be used in single test. + * + * @author Vyacheslav Rusakov + * @since 19.12.2022 + */ +public class StoredReusableApp implements ExtensionContext.Store.CloseableResource { + + private final Class declaration; + private final String source; + private final DropwizardTestSupport support; + private final ClientSupport client; + + /** + * Create stored app. + * + * @param declaration declaration test class + * @param source declaration source description + * @param support dropwizard test support + * @param client client + */ + public StoredReusableApp(final Class declaration, + final String source, + final DropwizardTestSupport support, + final ClientSupport client) { + this.declaration = declaration; + this.source = source; + this.support = support; + this.client = client; + } + + /** + * @return base test class where extension was declared + */ + public Class getDeclaration() { + return declaration; + } + + /** + * @return declaration source (base class + annotation or field name) + */ + public String getSource() { + return source; + } + + /** + * @return reusable support object + */ + public DropwizardTestSupport getSupport() { + return support; + } + + /** + * @return reusable client instance + */ + public ClientSupport getClient() { + return client; + } + + @Override + public void close() throws Exception { + support.after(); + client.close(); + } + + @Override + public String toString() { + return source; + } +} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java similarity index 55% rename from src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java rename to dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java index a396b06c1..c6790b931 100644 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/TestSetupUtils.java @@ -1,15 +1,19 @@ package ru.vyarus.dropwizard.guice.test.util; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; import org.junit.jupiter.api.extension.ExtensionContext; -import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.env.ListenersSupport; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.track.GuiceyTestTime; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.ServiceLoader; /** * Guicey {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} test utilities. @@ -22,6 +26,14 @@ public final class TestSetupUtils { private TestSetupUtils() { } + /** + * @return environment setup objects from service lookup + */ + public static TestEnvironmentSetup[] lookup() { + return Lists.newArrayList(ServiceLoader.load(TestEnvironmentSetup.class)) + .toArray(TestEnvironmentSetup[]::new); + } + /** * Instantiates provided support objects. * @@ -33,7 +45,7 @@ public static List create(final Class res = new ArrayList<>(); for (Class ext : extensions) { try { - res.add(ext.newInstance()); + res.add(InstanceUtils.create(ext)); } catch (Exception e) { throw new IllegalStateException("Failed to instantiate test support object: " + ext.getSimpleName(), e); } @@ -45,52 +57,58 @@ public static List create(final Class fields, final boolean includeInstanceFields) { - for (Field field : fields) { - if (!TestEnvironmentSetup.class.isAssignableFrom(field.getType())) { - throw new IllegalStateException(String.format( - "Field %s annotated with @%s, but its type is not %s", - toString(field), EnableSetup.class.getSimpleName(), - TestEnvironmentSetup.class.getSimpleName() - )); - } - if (!includeInstanceFields && !Modifier.isStatic(field.getModifiers())) { - throw new IllegalStateException(String.format("Field %s annotated with @%s must be static", - toString(field), EnableSetup.class.getSimpleName())); - } + public static String getContextTestName(final ExtensionContext context) { + // display name will show the correct name in case of groovy test (or will show @DisplayName value) + String res = context.getDisplayName(); + if (context.getTestMethod().isPresent()) { + res = context.getParent().get().getDisplayName() + '#' + res; } + return res; } - private static String toString(final Field field) { - return field.getDeclaringClass().getName() + "." + field.getName(); + private static Object setup(final TestEnvironmentSetup support, + final ExtensionConfig config, + final TestExtension builder) { + // required to recognize hooks registered from setup objects + config.tracker.setContextSetupObject(support.getClass()); + try { + final Object res = support.setup(builder); + config.tracker.setContextSetupObject(null); + return res; + } catch (Exception ex) { + Throwables.throwIfUnchecked(ex); + throw new IllegalStateException("Failed to run test setup object", ex); + } } /** @@ -99,6 +117,11 @@ private static String toString(final Field field) { public static class ClosableWrapper implements ExtensionContext.Store.CloseableResource { private final AutoCloseable obj; + /** + * Create wrapper. + * + * @param obj closable + */ public ClosableWrapper(final AutoCloseable obj) { this.obj = obj; } diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/EchoStream.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/EchoStream.java new file mode 100644 index 000000000..c18f16294 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/EchoStream.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.util.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Stream duplicates all writes into two streams. Used to write all output into console together with + * aggregation into separate stream (to be able to inspect output as string). + * + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +public class EchoStream extends OutputStream { + private final OutputStream out; + private final OutputStream collector; + + /** + * Create stream. + * + * @param out output streadm + */ + public EchoStream(final OutputStream out) { + this(out, new ByteArrayOutputStream()); + } + + /** + * Create stream. + * + * @param out output stream + * @param collector "echo" stream + */ + public EchoStream(final OutputStream out, final OutputStream collector) { + this.out = out; + this.collector = collector; + } + + @Override + public void write(final int i) throws IOException { + out.write(i); + collector.write(i); + } + + @Override + public String toString() { + return collector.toString(); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/SystemInMock.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/SystemInMock.java new file mode 100644 index 000000000..483955e91 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/test/util/io/SystemInMock.java @@ -0,0 +1,113 @@ +package ru.vyarus.dropwizard.guice.test.util.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; + +/** + * System in mock implementation. Should be used to substitute system input stream in order to mock user input in + * tests. {@code SystemInMock in = new SystemInMock(); System.setIn(in); in.provideText(..);}. + *

        + * Error would be thrown in case of not enough user inputs provided. + *

        + * Original code taken from System rules library (see + * TextFromStandardInputStream) with some modifications. + * + * @author Stefan Birkner + * @since 20.11.2023 + */ +public class SystemInMock extends InputStream { + private StringReader currentReader; + // with IOException, Scanner would intercept this exception and throw its own, whereas runtime exception + // would pass through (more suitable) + private final Supplier exception = () -> new IllegalStateException( + "Console input (" + getReads() + ") not provided"); + private int reads; + + /** + * Create system in substitutor. + */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") + public SystemInMock() { + provideText(); + } + + /** + * @param lines mock system in source + */ + public void provideText(final String... lines) { + final String separator = System.getProperty("line.separator"); + // all lines must end with a new line + currentReader = new StringReader(String.join(separator, lines) + separator); + } + + @Override + public int read() throws IOException { + final int character = currentReader.read(); + if (character == -1) { + throw exception.get(); + } + return character; + } + + @SuppressWarnings("PMD.AvoidThrowingNullPointerException") + @Override + public int read(final byte[] buffer, final int offset, final int len) throws IOException { + if (buffer == null) { + throw new NullPointerException(); + } else if (offset < 0 || len < 0 || len > buffer.length - offset) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } else { + return readNextLine(buffer, offset, len); + } + } + + /** + * @return count of user input requests + */ + public int getReads() { + return reads; + } + + private int readNextLine(final byte[] buffer, final int offset, final int len) + throws IOException { + // track user input requests count + reads++; + final int c = read(); + if (c == -1) { + return -1; + } + buffer[offset] = (byte) c; + + int i = 1; + for (; (i < len) && !isCompleteLineWritten(buffer, i - 1); ++i) { + final byte read = (byte) read(); + if (read == -1) { + break; + } else { + buffer[offset + i] = read; + } + } + return i; + } + + private boolean isCompleteLineWritten(final byte[] buffer, final int indexLastByteWritten) { + final byte[] separator = System.getProperty("line.separator").getBytes(StandardCharsets.UTF_8); + final int indexFirstByteOfSeparator = indexLastByteWritten - separator.length + 1; + return indexFirstByteOfSeparator >= 0 + && contains(buffer, separator, indexFirstByteOfSeparator); + } + + private boolean contains(final byte[] array, final byte[] pattern, final int indexStart) { + for (int i = 0; i < pattern.length; ++i) { + if (array[indexStart + i] != pattern[i]) { + return false; + } + } + return true; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/AppUrlBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/AppUrlBuilder.java new file mode 100644 index 000000000..f5f6c7fd5 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/AppUrlBuilder.java @@ -0,0 +1,280 @@ +package ru.vyarus.dropwizard.guice.url; + +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.UriBuilder; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.dropwizard.guice.url.util.AppPathUtils; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * General utility for building absolute application urls (counting current server configuration). + * This might be used for views, when (especially when proxy is used) it is often required to build absolute + * urls. Also, could be usd for resource redirections (but, in this case, + * {@link ru.vyarus.dropwizard.guice.url.RestPathBuilder} could be used directly). + *

        + * The default server creates two contexts: main (including rest) on 8080 and admin context on 8081. + * For simple server, there would be only one context where admin would be available on sub-url "/admin". + *

        + * Url-related configurations: + *

          + *
        • {@code server.applicationContextPath} - main context path
        • + *
        • {@code server.rootPath} - rest mapping url (main context path is also applied for rest)
        • + *
        • {@code server.adminContextPath} - admin context path
        • + *
        + *

        + * By default, class would create "localhost" based urls (because there is no way to know server host). + * To build for a particular host use {@link #forHost(String)} - this way server ports will be applied. + *

        + * Application could also be behind a proxy (like apache) which hides real server port. In this case use + * {@link #forProxy(String)} - server port would not be applied. Also, apache might be configured to + * serve application for a sub-path like "http://myhost.com/myapp/" - use this as a proxy url to build + * correct application urls. + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.url.util.AppPathUtils for configured ports and mappings direct resolution + * @see ru.vyarus.dropwizard.guice.url.RestPathBuilder for resource-only urls (not absolute) + * @since 26.09.2025 + */ +public class AppUrlBuilder { + + private final Supplier environment; + private final String host; + private final boolean proxied; + + /** + * Default constructor for "localhost"-based urls. It would be possible to create specialized builders with + * {@link #forHost(String)} or {@link #forProxy(String)} methods. + *

        + * Note: method annotated with {@link jakarta.inject.Inject} to be able to obtain builder directly from + * guice context. + * + * @param environment environment instance + */ + @Inject + public AppUrlBuilder(final Environment environment) { + this(() -> environment); + } + + /** + * Special constructor for instance creation before application startup. + * + * @param environment environment supplier + */ + public AppUrlBuilder(final Supplier environment) { + this(environment, "http://localhost", false); + } + + /** + * Create an app url builder. + * + * @param environment environment object + * @param host required host + * @param proxied true if server is proxied (port already declared in host) + */ + protected AppUrlBuilder(final Supplier environment, final String host, final boolean proxied) { + this.environment = environment; + this.host = host; + this.proxied = proxied; + } + + /** + * Create builder instance for specific host. The host must contain protocol and the host name like + * "https://myhost.com" and builder will append correct port and build urls relatively. + * + * @param host application host (and protocol) + * @return builder instance + */ + public AppUrlBuilder forHost(final String host) { + return new AppUrlBuilder(environment, host, false); + } + + /** + * Create builder instance for proxied application. In this case, real application port is not used + * (most commonly, apache would redirect port 80 into application port). Also, apache could be configured + * to serve application from a sub-path like "http://myhost.com/myapp/" (provided host myst point to the + * application root). + * + * @param proxy proxied host (and protocol) + * @return builder instance + */ + public AppUrlBuilder forProxy(final String proxy) { + return new AppUrlBuilder(environment, proxy, true); + } + + + // ----------------------------------------------------------------------------------- BUILDER METHODS + + /** + * Build url relative to application root ("/"). No server configurations applied in this case. + *

        + * Warning: don't mix root urls with the main context urls as application main context could differ. + *

        + * To get root url: {@code #root("/")}, + * + * @param path target path, relative to application root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return application path url + */ + public String root(final String path, final Object... args) { + final String root = proxied ? host + : AppPathUtils.getRooUrl(host, environment.get()); + return PathUtils.path(root, String.format(path, args)); + } + + /** + * Build url relative to application rest mapping. Configured main context path + * {@code server.applicationContextPath} and rest path {@code server.rootPath} would be prepended automatically. + *

        + * To get rest root url: {@code #rest("/")}, + * + * @param path target path, relative to rest root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return application resource url + * @see #rest(Class) for class-based resource url declaration + */ + public String rest(final String path, final Object... args) { + return PathUtils.path(baseRest(), String.format(path, args)); + } + + /** + * Build url relative to application rest mapping. Configured main context path + * {@code server.applicationContextPath} and rest path {@code server.rootPath} would be prepended automatically. + *

        + * Resource path is applied from resource {@link jakarta.ws.rs.Path} annotation. Resource method path could + * be declared with a method call: + * {@link ru.vyarus.dropwizard.guice.url.RestPathBuilder#method(ru.vyarus.dropwizard.guice.url.util.Caller)} + * (in this case all non-null arguments mapped as query and path params would be applied automatically). If + * resource-method-based declaration not used, then it might be required to provide manually values for declared + * path params ({@link ru.vyarus.dropwizard.guice.url.resource.ResourceParamsBuilder#pathParam(String, Object)}) + * and (optionally) apply query params. + *

        + * Example: + * {@code rest(MyResource.class).method(res -> res.doGet("abc", 12).build())}, where + * {@code public Response doGet(@PathParam("par1") String par1, @QueryParam("qp") int qp} and so + * path param "par1" and query param "qp" would be registered automatically (assuming path param used in resource + * path). + *

        + * If target method is in sub-resource, use + * {@link ru.vyarus.dropwizard.guice.url.RestPathBuilder#subResource(String, Class, Object...)} to append its path + * and switch context class. + *

        + * Direct resource class and method usage allows to get refactoring-safe urls. + * + * @param resource resource class + * @param resource type + * @return resource path builder + */ + public RestPathBuilder rest(final Class resource) { + return new RestPathBuilder<>(baseRest(), resource, false); + } + + /** + * Manual rest path building. Useful for complex cases with matrix params in the middle. + * + * @param path path builder + * @param resource target resource class + * @param resource type + * @return resource path builder + */ + public RestPathBuilder rest(final Consumer path, final Class resource) { + final UriBuilder builder = UriBuilder.newInstance(); + path.accept(builder); + // resource Path annotation is ignored + return new RestPathBuilder<>(rest(builder.toString()), resource, true); + } + + /** + * Build url relative to the main context ({@code server.applicationContextPath}). + *

        + * To get main context root url: {@code #main("/")}, + * + * @param path target path, relative to the main context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return application path url + */ + public String app(final String path, final Object... args) { + final String main = proxied + ? PathUtils.path(host, getAppContextPath()) + : AppPathUtils.getAppUrl(host, environment.get()); + return PathUtils.path(main, String.format(path, args)); + } + + /** + * Build url relative to the admin context ({@code server.adminContextPath}). + *

        + * To get admin context root url: {@code #admin("/")}, + * + * @param path target path, relative to the admin context (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return application path url + */ + public String admin(final String path, final Object... args) { + final String admin = proxied + ? PathUtils.path(host, getAdminContextPath()) + : AppPathUtils.getAdminUrl(host, environment.get()); + return PathUtils.path(admin, String.format(path, args)); + } + + // ----------------------------------------------------------------------------------- UTILS + + /** + * Get main application context port. + * + * @return the actual port the connector is listening to, or -1 if it has not been opened, or -2 if it has been + * closed. + */ + public int getAppPort() { + return AppPathUtils.getAppPort(environment.get()); + } + + /** + * Get admin application context port. + * + * @return the actual port the connector is listening to, or -1 if it has not been opened, or -2 if it has been + * closed. + */ + public int getAdminPort() { + return AppPathUtils.getAdminPort(environment.get()); + } + + /** + * Rest context is configured with {@code server.rootPath} and it is "/" by default. + * Also, rest is mapped under the main context, so if main context mapping changed with + * {@code server.applicationContextPath} it must also be counted. + * The returned path counts both and so is relative to the server root. + * + * @return rest context (with a context path), relative to the server root path + */ + public String getRestContextPath() { + return AppPathUtils.getRestContextPath(environment.get()); + } + + /** + * Main context is configured with {@code server.applicationContextPath} and it is "/" by default. + * + * @return main context (with a context path), relative to the server root path + */ + public String getAppContextPath() { + return AppPathUtils.getAppContextPath(environment.get()); + } + + /** + * Admin context is configured with {@code server.adminContextPath} and it is "/" by default + * ("/admin" for simple server). Note that by default, admin is on different port. + * + * @return main context (with a context path), relative to the server root path + */ + public String getAdminContextPath() { + return AppPathUtils.getAdminContextPath(environment.get()); + } + + private String baseRest() { + return proxied + ? PathUtils.path(host, getRestContextPath()) + : AppPathUtils.getRestUrl(host, environment.get()); + } + +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/RestPathBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/RestPathBuilder.java new file mode 100644 index 000000000..29654f91f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/RestPathBuilder.java @@ -0,0 +1,163 @@ +package ru.vyarus.dropwizard.guice.url; + +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.test.client.builder.call.RestCallAnalyzer; +import ru.vyarus.dropwizard.guice.url.model.ResourceMethodInfo; +import ru.vyarus.dropwizard.guice.url.resource.ResourceAnalyzer; +import ru.vyarus.dropwizard.guice.url.resource.ResourceParamsBuilder; +import ru.vyarus.dropwizard.guice.url.util.Caller; + +import java.lang.reflect.Method; + +/** + * Resource path builder. Used to build rest path using resource class and methods. + *

        + * Sub resources could be specified with {@link #subResource(String, Class, Object...)}. + *

        + * Path parameters and additional query parameters could be resolved with {@link #pathParam(String, Object)} and + * {@link #queryParam(String, Object...)}. Note that when resource path is built from method, required path and + * query parameters could be specified as resource methods arguments + * ({@link #method(ru.vyarus.dropwizard.guice.url.util.Caller)}). + *

        + * Final path could be built either as template (preserving path parameters) {@link #buildTemplate()} or as + * final path with resolved path parameters {@link #build()}. + * + * @param resource type + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.url.util.RestPathUtils for direct resource path resolution + * @since 26.09.2025 + */ +public class RestPathBuilder extends ResourceParamsBuilder { + + private final Class resource; + + /** + * Create a builder. + *

        + * Note that jersey ignores {@link jakarta.ws.rs.Path} annotation for sub resources (only lookup method path + * is used) + * + * @param basePath optional base path + * @param resource target resource + * @param subResource true if it is a sub resource (important because {@code @Path} annotation must be ignored) + */ + public RestPathBuilder(final @Nullable String basePath, final Class resource, final boolean subResource) { + super(basePath); + this.resource = resource; + // the same builder could be used for both resources and sub resources, so have to allow resources + // without root @Path annotation here + final String resourcePath = subResource ? null : ResourceAnalyzer.getResourcePath(resource); + if (resourcePath != null) { + builder.path(resourcePath); + } + } + + /** + * Append a sub-resource path to the current resource path (with sub-resource method resolution). + *

        + * Sub-resource path is declared by a sub-resource method: {@code @Path("/sub") SubResource something() {...}}. + * Here sub-resource path would include path from declaration method and path, declared for sub-resource class. + * The current method does not search for tareget declaration method (in some cases it is impossible), so + * sub-resource mapping path (from method) must be applied manually. + * + * @param path sub-resource mapping path (from sub-resource method; could contain String.format() + * placeholders: %s) + * @param subResource sub-resource + * @param args args variables for path placeholders (String.format() arguments) + * @param sub-resource type + * @return builder for sub-resource + */ + public RestPathBuilder subResource(final String path, final Class subResource, final Object... args) { + builder.path(String.format(path, args)); + final RestPathBuilder sub = new RestPathBuilder<>(builder.toString(), subResource, true); + sub.pathParams.putAll(pathParams); + return sub; + } + + /** + * Append a sub-resource path to the current resource path. Sub resource path is resolved through the called + * locator method. + *

        + * Note: multiple locator methods could be called at once! + * + * @param caller locator method(s) caller + * @param subResource sub-resource type + * @param sub-resource type + * @return builder for sub-resource + */ + public RestPathBuilder subResource(final Caller caller, final Class subResource) { + final String path = RestCallAnalyzer.getSubResourcePath(resource, caller); + builder.path(path); + final RestPathBuilder sub = new RestPathBuilder<>(builder.toString(), subResource, true); + sub.pathParams.putAll(pathParams); + return sub; + } + + /** + * Append provided path to resource path (useful when direct resource method can't be used for construction). + *

        + * Query parameters could be specified directly. + * + * @param path target path, relative to resource root (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @return parameters builder to specify path and query parameters + * @see #method(ru.vyarus.dropwizard.guice.url.util.Caller) + */ + public ResourceParamsBuilder path(final String path, final Object... args) { + applyPath(path, args); + return this; + } + + /** + * Append resource method path (path from {@link jakarta.ws.rs.Path} method annotation). + *

        + * When parameters are not provided - search for any method with name. If multiple methods + * with the same name would be found - method with no parameters would be selected, otherwise exception thrown. + * + * @param methodName method name + * @param parameters method argument types + * @return parameters builder to specify path and query parameters + * @throws java.lang.IllegalStateException if no unique method found for provided name + * @see #method(ru.vyarus.dropwizard.guice.url.util.Caller) + */ + public ResourceParamsBuilder method(final String methodName, final Class... parameters) { + if (parameters.length == 0) { + builder.path(ResourceAnalyzer.getMethodPath(resource, methodName)); + return this; + } else { + return method(ResourceAnalyzer.findMethod(resource, methodName, parameters)); + } + } + + /** + * Append resource method path (path from {@link jakarta.ws.rs.Path} method annotation). + * + * @param method resource method + * @return parameters builder to specify path and query parameters + * @see #method(ru.vyarus.dropwizard.guice.url.util.Caller) + */ + public ResourceParamsBuilder method(final Method method) { + ResourceAnalyzer.validateResourceMethod(resource, method); + builder.path(ResourceAnalyzer.getMethodPath(method)); + return this; + } + + /** + * Append resource method path (path from {@link jakarta.ws.rs.Path} method annotation). + * Also, use path and query providers, provided as method arguments. Note that only non-null parameters counted. + *

        + * Might also include sub resource call, if sub resource locator method returns resource instance: + * {@code resource.sub(args).method(args)} + * + * @param caller consumer with exactly one resource method execution + * @return parameters builder to specify path and query parameters + */ + public ResourceParamsBuilder method(final Caller caller) { + final ResourceMethodInfo info = ResourceAnalyzer.analyzeMethodCall(resource, caller); + builder.path(info.getPath()); + pathParams.putAll(info.getPathParams()); + info.getQueryParams().forEach(builder::queryParam); + info.getMatrixParams().forEach(builder::matrixParam); + return this; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/MethodCall.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/MethodCall.java new file mode 100644 index 000000000..b62bd4ee8 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/MethodCall.java @@ -0,0 +1,72 @@ +package ru.vyarus.dropwizard.guice.url.model; + +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Method; + +/** + * Recorded method call on resource class. The call may include sub-resource calls like: + * {@code resource.subResource(args).method(args)}. In this case the root would be a sub-resource method call, + * and a subCall would be a method call on sub-resource (there might be multiple sib-resource calls). + * + * @author Vyacheslav Rusakov + * @since 25.09.2025 + */ +public class MethodCall { + private final Class resource; + private final Method method; + private final Object[] args; + private MethodCall subCall; + + /** + * Create a method call info. + * + * @param resource resource type + * @param method called method + * @param args call arguments + */ + public MethodCall(final Class resource, final Method method, final Object... args) { + this.resource = resource; + this.method = method; + this.args = args; + } + + /** + * @return resource class where method was called + */ + public Class getResource() { + return resource; + } + + /** + * @return method instance + */ + public Method getMethod() { + return method; + } + + /** + * @return call arguments + */ + @SuppressWarnings("PMD.MethodReturnsInternalArray") + public Object[] getArgs() { + return args; + } + + /** + * @return sub method call + */ + @Nullable + public MethodCall getSubCall() { + return subCall; + } + + /** + * Set sub method call (call chain case). + * + * @param subCall sub method call + */ + public void setSubCall(final MethodCall subCall) { + this.subCall = subCall; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/ResourceMethodInfo.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/ResourceMethodInfo.java new file mode 100644 index 000000000..66e3a616f --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/model/ResourceMethodInfo.java @@ -0,0 +1,266 @@ +package ru.vyarus.dropwizard.guice.url.model; + +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The result of {@link MethodCall} analysis. Used to pre-fill request + * parameters by method annotations and provided arguments. + *

        + * For sub resource call, {@link #subResources} would not be empty, {@link #resource} would point to the ROOT + * resource and {@link #path} will be a method path, relative to resource root (include sub resource locator paths). + * + * @author Vyacheslav Rusakov + * @since 25.09.2025 + */ +public class ResourceMethodInfo { + + // resource class with annotations (but could be not for sub-resources) + private final Class resource; + private final String resourcePath; + + private final List> subResources; + // in case of sub resources each method call analyzed separately + private final List steps; + + // method with annotations! + private final Method method; + private final String path; + private final String httpMethod; + + private final List consumes = new ArrayList<>(); + private final List produces = new ArrayList<>(); + + private final Map pathParams = new HashMap<>(); + private final Map queryParams = new HashMap<>(); + private final Map headerParams = new HashMap<>(); + private final Map cookieParams = new HashMap<>(); + private final Map formParams = new HashMap<>(); + private final Map matrixParams = new HashMap<>(); + + private Object entity; + + /** + * Create a resource method info object. + * + * @param resource resource class + * @param resourcePath resource path + * @param method analyzed method + * @param path method path + * @param httpMethod http method + */ + public ResourceMethodInfo(final Class resource, + final String resourcePath, + final Method method, + final String path, + final @Nullable String httpMethod) { + this(resource, resourcePath, method, path, httpMethod, null, null); + } + + /** + * Create a resource method info object for the call chain (including sub resource locators). + * + * @param resource resource class + * @param resourcePath resource path + * @param method analyzed method + * @param path method path + * @param httpMethod http method + * @param subResources sub resource classes + * @param steps separate analysis for each method call in the chain + */ + public ResourceMethodInfo(final Class resource, + final String resourcePath, + final Method method, + final String path, + final @Nullable String httpMethod, + final @Nullable List> subResources, + final @Nullable List steps) { + this.resource = resource; + this.resourcePath = resourcePath; + this.method = method; + this.path = path; + this.httpMethod = httpMethod; + this.subResources = subResources == null ? Collections.emptyList() : subResources; + this.steps = steps == null ? Collections.emptyList() : steps; + } + + /** + * Note: for sub-resources it is not required to have root {@link jakarta.ws.rs.Path} annotation, for such + * classes here would be the root resource class. + * + * @return annotated resource class (where annotations declared) + */ + public Class getResource() { + return resource; + } + + /** + * @return sub resources or empty if direct resource method + */ + public List> getSubResources() { + return subResources; + } + + /** + * When sub resources used there would be multiple method calls {@code resource.subResource().method()}. + * In this case, each method call analyzed separately and, in some cases, non-aggregated data might be important + * (e.g. the only way to properly handle matrix params). + * + * @return separate methods analysis results for sub resource lookups or empty for single method call + */ + public List getSteps() { + return steps; + } + + /** + * @return resource path (without method), resolved from {@link jakarta.ws.rs.Path} annotation + */ + public String getResourcePath() { + return resourcePath; + } + + /** + * This might not be directly called method, but superclass or interface method, where jersey annotations + * were found (note that jersey does not collect annotations from different places - it expects everything to be + * configured in one place). + * + * @return method with jersey annotations + */ + public Method getMethod() { + return method; + } + + /** + * @return {@link jakarta.ws.rs.Consumes} annotation value from method or class + */ + public List getConsumes() { + return consumes; + } + + /** + * @return {@link jakarta.ws.rs.Produces} annotation value from method or clas + */ + public List getProduces() { + return produces; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.PathParam} + */ + public Map getPathParams() { + return pathParams; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.QueryParam} + */ + public Map getQueryParams() { + return queryParams; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.HeaderParam} + */ + public Map getHeaderParams() { + return headerParams; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.CookieParam} + */ + public Map getCookieParams() { + return cookieParams; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.FormParam} or + * {@link org.glassfish.jersey.media.multipart.FormDataParam} + */ + public Map getFormParams() { + return formParams; + } + + /** + * @return map of not null arguments, passed for {@link jakarta.ws.rs.MatrixParam}. + */ + public Map getMatrixParams() { + return matrixParams; + } + + /** + * @return method path, resolved from {@link jakarta.ws.rs.Path} annotation + */ + public String getPath() { + return path; + } + + /** + * @return http method, resolved from {@link jakarta.ws.rs.HttpMethod} annotation inside + * {@link jakarta.ws.rs.GET}, {@link jakarta.ws.rs.POST}, etc. + */ + public String getHttpMethod() { + return httpMethod; + } + + /** + * @return entity object + */ + public Object getEntity() { + return entity; + } + + /** + * Provided argument without known jersey annotations (assumed to be request body). + * + * @param entity entity object + */ + public void setEntity(final Object entity) { + this.entity = entity; + } + + /** + * @return full method path ({@link #getResourcePath()} with {@link #getPath()}) + */ + public String getFullPath() { + return PathUtils.path(resourcePath, path); + } + + /** + * Apply other method info to this one. + * + * @param other other data + */ + public void apply(final ResourceMethodInfo other) { + // assuming processing method chain from left to right and so more specific consume or produce annotations + // must override + if (!other.consumes.isEmpty()) { + this.consumes.clear(); + this.consumes.addAll(other.consumes); + } + if (!other.produces.isEmpty()) { + this.produces.clear(); + this.produces.addAll(other.produces); + } + + this.pathParams.putAll(other.pathParams); + this.queryParams.putAll(other.queryParams); + this.matrixParams.putAll(other.matrixParams); + this.headerParams.putAll(other.headerParams); + this.cookieParams.putAll(other.cookieParams); + this.formParams.putAll(other.formParams); + } + + @Override + public String toString() { + return resource.getSimpleName() + "." + TypeToStringUtils.toStringMethod(method, null) + + " (" + getFullPath() + ")"; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzer.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzer.java new file mode 100644 index 000000000..1da15b277 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzer.java @@ -0,0 +1,538 @@ +package ru.vyarus.dropwizard.guice.url.resource; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.PathSegment; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.dropwizard.guice.url.model.MethodCall; +import ru.vyarus.dropwizard.guice.url.model.ResourceMethodInfo; +import ru.vyarus.dropwizard.guice.url.util.Caller; +import ru.vyarus.dropwizard.guice.url.util.MultipartParamsSupport; +import ru.vyarus.java.generics.resolver.GenericsResolver; +import ru.vyarus.java.generics.resolver.util.TypeToStringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Utility to search jersey annotations on resource class. Jersey assumes that all annotations are declared on + * the same method! This could be a superclass method or implemented interface. + * + * @author Vyacheslav Rusakov + * @since 25.09.2025 + */ +@SuppressWarnings({"PMD.GodClass", "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects"}) +public final class ResourceAnalyzer { + + private ResourceAnalyzer() { + } + + /** + * Check that provided method belongs to resource class hierarchy (it might be superclass method or interface + * method). + * + * @param resource resource class + * @param method target method to verify + */ + public static void validateResourceMethod(final Class resource, final Method method) { + Preconditions.checkState(method.getDeclaringClass().isAssignableFrom(resource), + "Method '%s' does not belong to resource '%s'", + TypeToStringUtils.toStringMethod(method, null), resource.getSimpleName()); + } + + /** + * Search for {@link Path} annotation on resource class or it's superclasses or interfaces. + *

        + * The result is normalized: will always start with slash and will never end with slash + * (simpler for concatenation). + * + * @param resource resource class to analyze + * @return path annotation value + * @throws IllegalStateException if path annotation is not found on a resource or any of it's super classes and + * interfaces + */ + public static String getResourcePath(final Class resource) { + return PathUtils.normalizeAbsolutePath(findAnnotatedResource(resource).getAnnotation(Path.class).value()); + } + + /** + * Search for the actual annotated method (could be superclass or interface) and read the configured path. + *

        + * The result is normalized: will always start with slash and will never end with slash + * (simpler for concatenation). + * + * @param method resource method + * @return value of {@link jakarta.ws.rs.Path} annotation on metho + * @throws java.lang.IllegalStateException if annotation not found + */ + public static String getMethodPath(final Method method) { + final Path path = method.getAnnotation(Path.class); + if (path == null) { + // method might miss path annotation if the path is the same as resource path + return "/"; + } + return PathUtils.normalizeAbsolutePath(findAnnotatedMethod(method).getAnnotation(Path.class).value()); + } + + /** + * Search for resource method {@code @Path} annotation. If multiple methods selected, will select method + * without arguments. If there is no matching no-args method found throws exception (about multiple methods found). + * + * @param resource resource class + * @param method method name + * @return resource method {@code @Path} annotation value + * @throws java.lang.IllegalStateException if method with annotations not found + */ + public static String getMethodPath(final Class resource, final String method) { + final Method target = findMethod(resource, method); + return getMethodPath(target); + } + + /** + * Search for the actual resource annotations source (might be superclass of interface). + * + * @param resource resource class + * @return resource class or its superclass or interface where {@link jakarta.ws.rs.Path} annotation declared + */ + public static Class findAnnotatedResource(final Class resource) { + return getAnnotatedResource(resource).orElseThrow(() -> new IllegalStateException(String.format( + "@Path annotation was not found on resource %s or any of it's super classes and interfaces", + resource.getSimpleName() + ))); + } + + /** + * Shortcut for {@code class.getMethod(Par1.class, Par2.class)}. + * + * @param resource resource class + * @param methodName method name + * @param parameters method parameter types + * @return method instance + * @throws java.lang.IllegalStateException when method not found + */ + public static Method findMethod(final Class resource, final String methodName, final Class... parameters) { + final Method method; + try { + method = resource.getMethod(methodName, parameters); + } catch (Exception ex) { + throw new IllegalStateException(String.format("Method '%s(%s)' not found in class '%s'", + methodName, + Arrays.stream(parameters).map(Class::getSimpleName).collect(Collectors.joining(", ")), + resource.getSimpleName()), ex); + } + return method; + } + + /** + * Search for method with provided name in resource. If multiple methods selected, will select method + * without arguments. If there is no matching no-args method found throws exception (about multiple methods found). + * This no-args behavior is required to comply with arguments-based search (it would be otherwise impossible + * to search for no-args method). + *

        + * Returned method might be not the resource method, but a super method from a subclass or interface, + * where jersey annotations declared. + * + * @param resource resource clas + * @param method method name + * @return resource method with jersey annotations + * @throws java.lang.IllegalStateException if the method is not found or multiple methods found + */ + public static Method findMethod(final Class resource, final String method) { + final Method[] methods = resource.getMethods(); + final List found = new ArrayList<>(); + for (final Method m : methods) { + if (method.equals(m.getName()) && !m.isSynthetic()) { + found.add(m); + } + } + Preconditions.checkState(!found.isEmpty(), "Method '%s' not found in class '%s'", + method, resource.getSimpleName()); + if (found.size() > 1) { + // search for no-args method + final Method single = found.stream().filter(method1 -> method1.getParameterCount() == 0).findAny() + .orElseThrow(() -> + new IllegalStateException(String.format( + "Method with name '%s' is not unique in class '%s': %s", + method, resource.getSimpleName(), found.stream() + .map(method1 -> TypeToStringUtils.toStringMethod(method1, null)) + .collect(Collectors.joining(", ")))) + ); + found.clear(); + found.add(single); + } + + return findAnnotatedMethod(found.get(0)); + } + + /** + * Search method, annotated with {@link jakarta.ws.rs.Path}. Jersey supports declaring annotations in + * superclass or on interface, so the resource method may miss actual annotations. + * + * @param method method to search annotated for + * @return annotated method + * @throws java.lang.IllegalStateException if method with annotations not found + */ + public static Method findAnnotatedMethod(final Method method) { + Method res = null; + // searching for annotated method (could be Path or method annotation) + if (!isJerseyAnnotated(method)) { + // try to search in superclasses and interfaces (for declaring class!) + for (Class type : GenericsResolver.resolve(method.getDeclaringClass()) + .getGenericsInfo().getComposingTypes()) { + for (Method cand : type.getDeclaredMethods()) { + // searching same method, but annotated + if (cand.getName().equals(method.getName()) + && cand.getParameterTypes().length == method.getParameterTypes().length + // not count possible type differences + && Arrays.equals(cand.getParameterTypes(), method.getParameterTypes()) + && isJerseyAnnotated(cand)) { + res = cand; + break; + } + } + if (res != null) { + break; + } + } + Preconditions.checkState(res != null, "Annotated method %s was not found in class hierarchy of %s", + TypeToStringUtils.toStringMethod(method, null), method.getDeclaringClass().getSimpleName()); + } else { + res = method; + } + return res; + } + + /** + * Check if provided method is annotated with jersey annotations. Http methods must have http method + * annotation (like {@link jakarta.ws.rs.GET}), but may lack {@link jakarta.ws.rs.Path} annotation. + * Sub-resource lookup method must have {@link Path annotation}. So target method must be checked to contain + * one of possible annotations. + *

        + * Note that it is impossible to have Path and http method annotation on different methods (jersey requirement). + * + * @param method method to check + * @return true if method contains jersey annotations + */ + public static boolean isJerseyAnnotated(final Method method) { + return method.getAnnotation(Path.class) != null || getHttpMethod(method).isPresent(); + } + + /** + * Resolve http method by searching for method annotations like {@link jakarta.ws.rs.GET} or + * {@link jakarta.ws.rs.POST}. + *

        + * WARNING: does not search the correct annotated method (see + * {@link #findAnnotatedMethod(java.lang.reflect.Method)}) - assumed correct method was already found. + * + * @param method method to read annotations on + * @return http method string (from method annotations) + * @throws java.lang.IllegalStateException if annotation not found + */ + public static String findHttpMethod(final Method method) { + return getHttpMethod(method).orElseThrow(() -> new IllegalStateException(String.format( + "Http method type annotation was not found on resource method: %s", + TypeToStringUtils.toStringMethod(method, null) + ))); + } + + /** + * Search http annotations on method (like {@link jakarta.ws.rs.GET}). + * + * @param method method to find annotation on + * @return http method or null + */ + public static Optional getHttpMethod(final Method method) { + String httpMethod = null; + for (Annotation ann : method.getAnnotations()) { + if (ann.annotationType().isAnnotationPresent(HttpMethod.class)) { + httpMethod = ann.annotationType().getAnnotation(HttpMethod.class).value(); + } + } + return Optional.ofNullable(httpMethod); + } + + /** + * Use resource method call to collect data, required to call this rest method (like method path, query or path + * params). Method associate provided method arguments with data required for call (like pre-declared + * query or path params). + *

        + * Provided consumer must call one resource method with any arguments: + * {@code analyzeMethod(Res.class, Res mock -> mock.someMethod(12, "foo", null))} + * where resource method is: + * {@code Response someMethod(@QueryParam("p1") int p1, @PathParam("pp") String pp, @Context UriInfo info}. + * From the call above we can: + *

          + *
        • find {@link java.nio.file.Path} method annotation to get the path
        • + *
        • find http method annotation
        • + *
        • record required query param value: p1=12
        • + *
        • record required path param value: pp="foo"
        • + *
        + *

        + * Sub method calls assumed to be a sub-resource calls like {@code resource.subResource(args).method(args)}. + * For sub-resource calls, returned info will contain root resource as base resource, but resource method + * path will include all paths from locator methods (class-level {@link jakarta.ws.rs.Path} annotation is + * ignored for sub-resources!). + * + * @param resource resource class + * @param consumer consumer calling exactly one resource method + * @param resource type + * @return method analysis info + */ + public static ResourceMethodInfo analyzeMethodCall(final Class resource, final Caller consumer) { + final List methods = ResourceMethodLookup.getMethodCalls(resource, consumer); + Preconditions.checkState(methods.size() == 1, "Only one resource method call is required, but %s recorded", + methods.size()); + return analyzeMethodCall(methods.get(0)); + } + + /** + * Analyze resource method, using method call arguments. Recognize query params, path params, headers, cookies, + * etc. from method parameter annotations for non-null arguments. + *

        + * Sub method calls assumed to be a sub-resource calls like {@code resource.subResource(args).method(args)}. + * For sub-resource calls, returned info will contain root resource as base resource, but resource method + * path will include all paths from locator methods (class-level {@link jakarta.ws.rs.Path} annotation is + * ignored for sub-resources!). + *

        + * Method call could be intercepted with + * {@link ResourceMethodLookup#getMethodCalls(Class, ru.vyarus.dropwizard.guice.url.util.Caller)}. + * + * @param call intercepted method call + * @return analysis result + */ + public static ResourceMethodInfo analyzeMethodCall(final MethodCall call) { + final List infos = new ArrayList<>(); + MethodCall current = call; + while (current != null) { + infos.add(analyzeSingleCall(current)); + current = current.getSubCall(); + } + + // simple case + if (infos.size() == 1) { + return infos.get(0); + } + + // sub resource call + + // creating a result based on the last call (actual sub-resource method call) + final ResourceMethodInfo last = infos.get(infos.size() - 1); + final ResourceMethodInfo first = infos.get(0); + // full path starting from the first resource (including all methods and sub resources) + final List resourcePath = new ArrayList<>(); + final List> subResources = new ArrayList<>(); + for (ResourceMethodInfo info : infos) { + subResources.add(info.getResource()); + // only method path counted! + resourcePath.add(info.getPath()); + } + + // consider sub resources as part of the method call (method call relative to the root resource) + final ResourceMethodInfo res = new ResourceMethodInfo(first.getResource(), + // root resource path + first.getResourcePath(), + last.getMethod(), + // only lookup methods path is counted + actual resource method + PathUtils.path(resourcePath.toArray(new String[0])), + last.getHttpMethod(), + // sub resource classes, excluding root resource + subResources.subList(1, subResources.size()), + infos); + + // apply data from left to right (right data should override left) + infos.forEach(res::apply); + + return res; + } + + /** + * Analyze method call, ignoring sub calls {@link MethodCall#getSubCall()}. + * See {@link #analyzeMethodCall(MethodCall)} for complete analysis. + * + * @param call method call + * @return single call analysis info + */ + public static ResourceMethodInfo analyzeSingleCall(final MethodCall call) { + // jersey restriction: all annotations MUST be on the same method! + // the resource might not contain annotation in case of sub-resource + final Class annotatedResource = getAnnotatedResource(call.getResource()).orElse(null); + final Method annotated = findAnnotatedMethod(call.getMethod()); + final String path = getMethodPath(annotated); + // sub-resource locator may lack http method annotation + final String httpMethod = getHttpMethod(annotated).orElse(null); + final ResourceMethodInfo info = new ResourceMethodInfo( + MoreObjects.firstNonNull(annotatedResource, call.getResource()), + annotatedResource == null ? "/" : getResourcePath(annotatedResource), + annotated, path, httpMethod); + + detectMediaTypes(info, info.getResource(), annotated); + + // analyze parameters + final Multimap multipart = ArrayListMultimap.create(); + for (int i = 0; i < call.getArgs().length; i++) { + final Object arg = call.getArgs()[i]; + handle(annotated.getParameterAnnotations()[i], arg, info, multipart); + } + if (!multipart.isEmpty()) { + // multipart params often doubled (stream with metadata) so need to process all at once + MultipartParamsSupport.processFormParams(info.getFormParams(), multipart); + } + return info; + } + + private static Optional> getAnnotatedResource(final Class resource) { + Path ann = resource.getAnnotation(Path.class); + Class res = resource; + + if (ann == null) { + // try to search in superclasses and interfaces + for (Class type : GenericsResolver.resolve(resource).getGenericsInfo().getComposingTypes()) { + ann = type.getAnnotation(Path.class); + if (ann != null) { + res = type; + break; + } + } + } + return Optional.ofNullable(ann == null ? null : res); + } + + private static void detectMediaTypes(final ResourceMethodInfo info, + final Class resource, + final Method method) { + Consumes consumes = method.getAnnotation(Consumes.class); + Produces produces = method.getAnnotation(Produces.class); + + if (consumes == null) { + consumes = resource.getAnnotation(Consumes.class); + } + if (produces == null) { + produces = resource.getAnnotation(Produces.class); + } + + if (consumes != null) { + Collections.addAll(info.getConsumes(), consumes.value()); + // clear default + info.getConsumes().remove("*/*"); + } + if (produces != null) { + Collections.addAll(info.getProduces(), produces.value()); + // clear default + info.getProduces().remove("*/*"); + } + } + + @SuppressWarnings({"unchecked", "PMD.NcssCount", "PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", + "checkstyle:CyclomaticComplexity", "checkstyle:JavaNCSS", "checkstyle:ExecutableStatementCount"}) + private static void handle(final Annotation[] annotations, final Object value, final ResourceMethodInfo info, + final Multimap multipart) { + boolean recognized = false; + if (value != null) { + for (Annotation ann : annotations) { + if (ann.annotationType().equals(Context.class)) { + // avoid handling special parameters + return; + } else if (ann.annotationType().equals(PathParam.class)) { + final PathParam param = (PathParam) ann; + // path segments could be used for matrix params declaration in the middle + if (!(value instanceof PathSegment)) { + info.getPathParams().put(param.value(), value); + } + recognized = true; + } else if (ann.annotationType().equals(QueryParam.class)) { + final QueryParam param = (QueryParam) ann; + info.getQueryParams().put(param.value(), value); + recognized = true; + } else if (ann.annotationType().equals(HeaderParam.class)) { + final HeaderParam param = (HeaderParam) ann; + info.getHeaderParams().put(param.value(), value); + recognized = true; + } else if (ann.annotationType().equals(MatrixParam.class)) { + final MatrixParam param = (MatrixParam) ann; + info.getMatrixParams().put(param.value(), value); + recognized = true; + } else if (ann.annotationType().equals(FormParam.class)) { + final FormParam param = (FormParam) ann; + info.getFormParams().put(param.value(), value); + recognized = true; + } else if (ann.annotationType().equals(CookieParam.class)) { + final CookieParam param = (CookieParam) ann; + info.getCookieParams().put(param.value(), String.valueOf(value)); + recognized = true; + } else if ("FormDataParam".equals(ann.annotationType().getSimpleName())) { + // collected for delayed processing + final String paramName = MultipartParamsSupport.getParamName(ann); + if (value instanceof Collection) { + ((Collection) value).forEach(o -> multipart.put(paramName, o)); + } else { + multipart.put(paramName, value); + } + recognized = true; + } else if ("org.glassfish.jersey.media.multipart.FormDataMultiPart".equals( + value.getClass().getName())) { + // case: method parameter accept entire multipart (without direct fields mapping) + MultipartParamsSupport.configureFromMultipart(multipart, value); + recognized = true; + } else if (ann.annotationType().equals(BeanParam.class)) { + handleBean(value, info, multipart); + recognized = true; + } + } + // this could be urlencoded parameters + if (value instanceof MultivaluedMap) { + final MultivaluedMap map = (MultivaluedMap) value; + map.forEach((o, objects) -> + info.getFormParams().put(String.valueOf(o), objects.size() == 1 ? objects.get(0) : objects) + ); + recognized = true; + } + if (!recognized) { + // assume it is a method body (e.g. POST entity). + // The logic: if used provided not null value then it should be used in request + Preconditions.checkState(info.getEntity() == null, "Multiple entity arguments detected: \n\t%s\n\t%s", + info.getEntity(), value); + info.setEntity(value); + } + } + } + + private static void handleBean(final Object bean, final ResourceMethodInfo info, + final Multimap multipart) { + final Field[] fields = bean.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + final Object value; + try { + value = field.get(bean); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to introspect @BeanParam", e); + } + if (value != null) { + handle(field.getAnnotations(), value, info, multipart); + } + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceMethodLookup.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceMethodLookup.java new file mode 100644 index 000000000..e3b285153 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceMethodLookup.java @@ -0,0 +1,156 @@ +package ru.vyarus.dropwizard.guice.url.resource; + +import com.google.common.base.Preconditions; +import javassist.util.proxy.MethodHandler; +import javassist.util.proxy.Proxy; +import javassist.util.proxy.ProxyFactory; +import javassist.util.proxy.ProxyObject; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; +import ru.vyarus.dropwizard.guice.url.model.MethodCall; +import ru.vyarus.dropwizard.guice.url.util.Caller; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Consumer; + +/** + * Utility for intercepting resource call (on special proxy object) to extract called method and provided arguments. + * This is useful for simplifying target method path building (based on jersey annotations). + * + * @author Vyacheslav Rusakov + * @since 25.09.2025 + */ +public final class ResourceMethodLookup { + + private ResourceMethodLookup() { + } + + /** + * Procide special proxy object into consumer in order to intercept method call and record target method + * with called arguments. + * + * @param resource resource method + * @param call consumer with method(s) call + * @param resource type + * @return called methods + */ + public static List getMethodCalls(final Class resource, final Caller call) { + final T proxy = createLookupProxy(resource, null); + try { + call.call(proxy); + } catch (Exception e) { + throw new IllegalStateException(String.format("Failed to record method call on resource '%s'", + resource.getSimpleName()), e); + } + return getMethodCalls(proxy); + } + + /** + * Extract recorded method calls from proxy. Throw exception if no methods were called. + * + * @param proxy proxy instance created by {@link #createLookupProxy(Class, java.util.function.Consumer)} + * @return list of called methods + */ + private static List getMethodCalls(final Object proxy) { + return ((Handler) ((ProxyObject) proxy).getHandler()).getCalls(); + } + + /** + * Create a proxy from a resource class. Proxy would record all method calls (without calling actual class methods). + * Use {@link #getMethodCalls(Object)} to get recorded calls. + * + * @param resource resource class + * @param callHandler optional direct call handler, used for sub-method calls processing + * @param resource type + * @return proxy instance + */ + @SuppressWarnings("unchecked") + private static T createLookupProxy(final Class resource, final Consumer callHandler) { + Preconditions.checkArgument(resource != null, "Resource class is required"); + final ProxyFactory factory = new ProxyFactory(); + factory.setSuperclass(resource); + final T proxy = InstanceUtils.create((Class) factory.createClass()); + + final Handler handler = new Handler(resource, callHandler); + ((Proxy) proxy).setHandler(handler); + return proxy; + } + + /** + * Handler, intercepting proxy method calls. + */ + public static class Handler implements MethodHandler { + private final Class resource; + private final List calls = new java.util.ArrayList<>(); + private final Consumer subCallHandler; + + /** + * Create a method interceptor. + * + * @param resource resource class + * @param subCallHandler optional direct call handler, used for sub-method calls processing + */ + public Handler(final Class resource, final @Nullable Consumer subCallHandler) { + this.resource = resource; + this.subCallHandler = subCallHandler; + } + + @Override + @SuppressWarnings("checkstyle:ReturnCount") + public Object invoke(final Object self, final Method thisMethod, final Method proceed, + final Object[] args) throws Throwable { + final MethodCall call = new MethodCall(resource, thisMethod, args); + calls.add(call); + + // handling sub-method call with direct listener + if (subCallHandler != null) { + subCallHandler.accept(call); + } + + if (!thisMethod.getReturnType().equals(void.class) + && !thisMethod.getReturnType().getPackageName().startsWith("java.") + && !thisMethod.getReturnType().getPackageName().startsWith("jakarta.")) { + return createLookupProxy(thisMethod.getReturnType(), call::setSubCall); + } + if (thisMethod.getReturnType().isPrimitive()) { + // required to avoid null pointer exceptions + return createPrimitive(thisMethod.getReturnType()); + } + // just record call, no actual method execution required + return null; + } + + // hack required to correctly handle methods with primitive return type + @SuppressWarnings("checkstyle:ReturnCount") + private Object createPrimitive(final Class type) { + if (type == byte.class) { + return (byte) 0; + } else if (type == long.class) { + return (long) 0; + } else if (type == short.class) { + return (short) 0; + } else if (type == int.class) { + return 0; + } else if (type == float.class) { + return (float) 0; + } else if (type == double.class) { + return (double) 0; + } else if (type == boolean.class) { + return false; + } else if (type == char.class) { + return '\0'; + } + return 0; + } + + /** + * @return recorded calls + * @throws java.lang.IllegalStateException if nothing recorded + */ + public List getCalls() { + Preconditions.checkState(!calls.isEmpty(), "No method calls recorded"); + return calls; + } + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceParamsBuilder.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceParamsBuilder.java new file mode 100644 index 000000000..2da9f3e3d --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/resource/ResourceParamsBuilder.java @@ -0,0 +1,136 @@ +package ru.vyarus.dropwizard.guice.url.resource; + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import org.glassfish.jersey.uri.internal.JerseyUriBuilder; +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; + +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Resource path params builder. Allows path and query params declaration. + * Used as the last step for {@link ru.vyarus.dropwizard.guice.url.RestPathBuilder} (to hide other path-modifying + * methods). + */ +public abstract class ResourceParamsBuilder { + /** + * Path builder. + */ + protected final JerseyUriBuilder builder = new JerseyUriBuilder(); + /** + * Path parameters map. + */ + protected final Map pathParams = new HashMap<>(); + + /** + * Construct a builder. + * + * @param basePath optional base path (to prepend) + */ + @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") + public ResourceParamsBuilder(final @Nullable String basePath) { + if (basePath != null) { + applyPath(PathUtils.normalizeAbsolutePath(basePath)); + } + } + + /** + * Apply path parameter. + * + * @param name parameter name + * @param value parameter value + * @return builder instance for chained calls + */ + public ResourceParamsBuilder pathParam(final String name, final Object value) { + pathParams.put(name, value); + return this; + } + + /** + * Apply query parameter. + *

        + * Note: jersey api supports multiple query parameters with the same name (in this case multiple parameters + * added). + * + * @param name parameter name + * @param values one or more parameter values (array for multiple values) + * @return builder instance for chained calls + */ + public ResourceParamsBuilder queryParam(final String name, final Object... values) { + builder.queryParam(name, values); + return this; + } + + /** + * Apply matrix parameter. + *

        + * Note: jersey api supports multiple matrix parameters with the same name (in this case multiple parameters + * added). + * + * @param name parameter name + * @param values one or more parameter values (array for multiple values) + * @return builder instance for chained calls + */ + public ResourceParamsBuilder matrixParam(final String name, final Object... values) { + builder.matrixParam(name, values); + return this; + } + + /** + * @return path with preserved path parameters placeholders + */ + public String buildTemplate() { + return builder.toTemplate(); + } + + /** + * @return path with resolved path parameters + */ + public String build() { + return pathParams.isEmpty() ? builder.toString() + : builder.resolveTemplatesFromEncoded(pathParams).toString(); + } + + /** + * Useful for resource redirects: ({@code Response.sendRedirect(builder...buildUri()).build()}). + * + * @return uri from configured path (with resolved variables) + */ + public URI buildUri() { + final String res = build(); + try { + return new URI(res); + } catch (Exception e) { + throw new IllegalStateException("Failed to build URI from: " + res, e); + } + } + + /** + * Apply path containing query parameters. Manual parsing is required because otherwise query paramteres + * would be incorrectly encoded. + * + * @param path path (possibly with query parameters) with possible string format placeholders (%s) + * @param args optional path parameters placeholders (String.format() arguments) + */ + protected void applyPath(final String path, final Object... args) { + // manually parse query params because otherwise ? would be encoded + final int idx = path.indexOf('?'); + final String result = String.format(path, args); + String target = result; + final Multimap params = LinkedHashMultimap.create(); + if (idx > 0) { + target = result.substring(0, idx); + final String query = result.substring(idx + 1); + Arrays.stream(query.split("&")).forEach(s -> { + final String[] pair = s.split("="); + params.put(pair[0], pair.length == 1 ? "" : pair[1]); + }); + } + builder.path(target); + params.keySet().forEach(s -> queryParam(s, params.get(s).toArray(new Object[0]))); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/AppPathUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/AppPathUtils.java new file mode 100644 index 000000000..8c520f6af --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/AppPathUtils.java @@ -0,0 +1,170 @@ +package ru.vyarus.dropwizard.guice.url.util; + +import com.google.common.base.Preconditions; +import io.dropwizard.core.setup.Environment; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ServerConnector; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; + +/** + * Utilities to resolve url-related server configuration (ports, mappings). + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.url.AppUrlBuilder + * @since 26.09.2025 + */ +public final class AppPathUtils { + + private AppPathUtils() { + } + + /** + * Get main application context port. + * + * @param environment environment instance + * @return the actual port the connector is listening to, or -1 if it has not been opened, or -2 if it has been + * closed. + */ + public static int getAppPort(final Environment environment) { + return getAppConnector(environment).getLocalPort(); + } + + /** + * Get admin application context port. + * + * @param environment environment instance + * @return the actual port the connector is listening to, or -1 if it has not been opened, or -2 if it has been + * closed. + */ + public static int getAdminPort(final Environment environment) { + return getAdminConnector(environment).getLocalPort(); + } + + /** + * Rest context is configured with {@code server.rootPath} and it is "/" by default. + * Also, rest is mapped under the main context, so if main context mapping changed with + * {@code server.applicationContextPath} it must also be counted. + * The returned path counts both and so is relative to the server root. + * + * @param environment environment instance + * @return rest context (with a context path), relative to the server root path + */ + public static String getRestContextPath(final Environment environment) { + final String contextPath = Preconditions.checkNotNull(environment.getJerseyServletContainer(), + "No started web application") + .getServletConfig().getServletContext().getContextPath(); + // server.rootPath + final String restMapping = PathUtils.trailingSlash(PathUtils.trimStars(environment.jersey().getUrlPattern())); + return PathUtils.trailingSlash( + PathUtils.path(contextPath, restMapping)); + } + + /** + * Main context is configured with {@code server.applicationContextPath} and it is "/" by default. + * + * @param environment environment instance + * @return main context (with a context path), relative to the server root path + */ + public static String getAppContextPath(final Environment environment) { + return PathUtils.trailingSlash(PathUtils.path(environment.getApplicationContext().getContextPath())); + } + + /** + * Admin context is configured with {@code server.adminContextPath} and it is "/" by default + * ("/admin" for simple server). Note that by default, admin is on different port. + * + * @param environment environment instance + * @return main context (with a context path), relative to the server root path + */ + public static String getAdminContextPath(final Environment environment) { + return PathUtils.trailingSlash(PathUtils.path(environment.getAdminContext().getContextPath())); + } + + /** + * Get the root application url (note: it could be different from rest or main context!). Assumed application is + * accessible by configured url without a proxy. So for default server config, the call + * {@code getRootUrl("http://myhost.com", environment)} will return "http://myhost:8080/". + *

        + * If application is behind a proxy, hiding application port and, for example, applying some prefix + * (e.g., apache redirect "http://myhost.com/my-app" to "http://localhost:8080/"), then this method can't be used - + * use proxy prefix directly: {@code "http://myhost.com/my-app/"}. + * + * @param host base host, including protocol and host name (like http://myhost.com) + * @param environment environment instance + * @return root application url + */ + public static String getRooUrl(final String host, final Environment environment) { + return formatUrl(host, getAppPort(environment), "/"); + } + + /** + * Get the main application url. Assumed application is accessible by configured url without a proxy. + * So for default server config, the call {@code getMainUrl("http://myhost.com", environment)} + * will return "http://myhost:8080/". + *

        + * If application is behind a proxy, hiding application port and, for example, applying some prefix + * (e.g., apache redirect "http://myhost.com/my-app" to "http://localhost:8080/"), then this method can't be used - + * use only prefix in this case: {@code "http://myhost.com/my-app" + getMainContextPath(environment)}. + * + * @param host base host, including protocol and host name (like http://myhost.com) + * @param environment environment instance + * @return main application url (with a context path) + */ + public static String getAppUrl(final String host, final Environment environment) { + return formatUrl(host, getAppPort(environment), getAppContextPath(environment)); + } + + /** + * Get the admin application url. Assumed application is accessible by configured url without a proxy. + * So for default server config, the call {@code getAdminUrl("http://myhost.com", environment)} + * will return "http://myhost:8081/" (for simple server: "http://myhost:8080/admin"). + *

        + * If application is behind a proxy, hiding application port and, for example, applying some prefix + * (e.g., apache redirect "http://myhost.com/my-app" to "http://localhost:8080/"), then this method can't be used - + * use only prefix in this case: {@code "http://myhost.com/my-app" + getAdminContextPath(environment)}. + * + * @param host base host, including protocol and host name (like http://myhost.com) + * @param environment environment instance + * @return admin application url (with a context path) + */ + public static String getAdminUrl(final String host, final Environment environment) { + return formatUrl(host, getAdminPort(environment), getAdminContextPath(environment)); + } + + /** + * Get the rest application url. Assumed application is accessible by configured url without a proxy. + * So for default server config, the call {@code getRestUrl("http://myhost.com", environment)} + * will return "http://myhost:8080/". + *

        + * If application is behind a proxy, hiding application port and, for example, applying some prefix + * (e.g., apache redirect "http://myhost.com/my-app" to "http://localhost:8080/"), then this method can't be used - + * use only prefix in this case: {@code "http://myhost.com/my-app" + getRestContextPath(environment)}. + * + * @param host base host, including protocol and host name (like http://myhost.com) + * @param environment environment instance + * @return rest application url (with a context path) + */ + public static String getRestUrl(final String host, final Environment environment) { + return formatUrl(host, getAppPort(environment), getRestContextPath(environment)); + } + + private static ServerConnector getAppConnector(final Environment environment) { + return (ServerConnector) getConnectors(environment)[0]; + } + + private static ServerConnector getAdminConnector(final Environment environment) { + final Connector[] connectors = getConnectors(environment); + return ((ServerConnector) connectors[connectors.length - 1]); + } + + private static Connector[] getConnectors(final Environment environment) { + return Preconditions.checkNotNull(environment.getApplicationContext().getServer(), + "No started web application").getConnectors(); + } + + private static String formatUrl(final String host, final int port, final String context) { + Preconditions.checkState(host.toLowerCase().startsWith("http"), + "Host must include target server protocol and the host name (like http://myhost.com)"); + return PathUtils.path(String.format("%s:%s", host, port), context); + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/Caller.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/Caller.java new file mode 100644 index 000000000..b5dcc9545 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/Caller.java @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.url.util; + +/** + * Interface used for recording of resource method call. + * + * @author Vyacheslav Rusakov + * @since 30.09.2025 + * @param instance type + */ +@FunctionalInterface +public interface Caller { + + /** + * Record instance method call. + * + * @param instance object proxy (to record call on) + * @throws Exception bypass all exceptions + */ + void call(T instance) throws Exception; +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/MultipartParamsSupport.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/MultipartParamsSupport.java new file mode 100644 index 000000000..00e43b789 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/MultipartParamsSupport.java @@ -0,0 +1,118 @@ +package ru.vyarus.dropwizard.guice.url.util; + +import com.google.common.collect.Multimap; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; + +/** + * Multipart parameters handling. Externalized into a separate class to support general paramteres usage without + * multipart dependency. + * + * @author Vyacheslav Rusakov + * @since 03.10.2025 + */ +public final class MultipartParamsSupport { + + private MultipartParamsSupport() { + } + + /** + * @param annotation param annotation + * @return param annotation value + */ + public static String getParamName(final Annotation annotation) { + return ((FormDataParam) annotation).value(); + } + + /** + * Parse entire multipart object {@link FormDataMultiPart} (it might be used as rest method parameter to + * aggregate all fields). + * + * @param params params collector + * @param value multipart value + */ + public static void configureFromMultipart(final Multimap params, final Object value) { + ((FormDataMultiPart) value).getFields().forEach((s, parts) -> { + final Object res = computeParameter(s, parts); + if (res != null) { + params.put(s, res); + } + }); + } + + /** + * Multipart parameters could be handled with multuiple aprameters, for example: + * {@code method(@FormDaraParam("file") InputStream in, @FormDataParam("file") FormDataContentDisposition info)}. + * + * @param params target map to store parameters + * @param multipart recorded field arguments + */ + public static void processFormParams(final Map params, + final Multimap multipart) { + for (String key : multipart.keySet()) { + final Collection values = multipart.get(key); + final Object res = computeParameter(key, values); + if (res != null) { + params.put(key, res); + } + } + } + + /** + * Recognize multipart value from the method call arguments. + * + * @param name field name + * @param values all arguments for the field + * @return recognized multipart value or null + */ + @SuppressWarnings({"PMD.CyclomaticComplexity", "checkstyle:ReturnCount", "checkstyle:CyclomaticComplexity"}) + public static Object computeParameter(final String name, final Collection values) { + InputStream stream = null; + String fileName = null; + final boolean singleValue = values.size() == 1; + for (Object value : values) { + if (value instanceof FormDataContentDisposition) { + fileName = ((FormDataContentDisposition) value).getFileName(true); + } else if (value instanceof FormDataBodyPart) { + final FormDataBodyPart part = (FormDataBodyPart) value; + if (part instanceof FileDataBodyPart) { + // raw file + return ((FileDataBodyPart) part).getFileEntity(); + } else if (value instanceof StreamDataBodyPart) { + // stream with filename + return value; + } else if (part.isSimple()) { + return part.getValue(); + } + // otherwise ignore value + } else if (value instanceof InputStream) { + if (singleValue) { + return value; + } else { + // case InputStream + FormDataContentDisposition + stream = (InputStream) value; + } + } else { + return value; + } + } + if (fileName != null && stream != null) { + return new StreamDataBodyPart(name, stream, fileName); + } else if (fileName != null) { + // very special case when the resource method declares only content disposition parameters + // and user specified value(s) for this argument: preserve file names, with fake stream value + return new StreamDataBodyPart(name, new ByteArrayInputStream(new byte[0]), fileName); + } + return null; + } +} diff --git a/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/RestPathUtils.java b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/RestPathUtils.java new file mode 100644 index 000000000..46d8a4ba8 --- /dev/null +++ b/dropwizard-guicey/src/main/java/ru/vyarus/dropwizard/guice/url/util/RestPathUtils.java @@ -0,0 +1,103 @@ +package ru.vyarus.dropwizard.guice.url.util; + +import org.jspecify.annotations.Nullable; +import ru.vyarus.dropwizard.guice.url.RestPathBuilder; + +/** + * Rest path utils. Useful for building resource paths based on resource classes and methods in application logic + * (for example, building redirects). + *

        + * There are two benefits of using classes directly: + *

          + *
        • Simplified code navigation (obvious where this path leads)
        • + *
        • Safety: if the path changed in class, it would be counted here
        • + *
        + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.url.RestPathBuilder + * @since 25.09.2025 + */ +public final class RestPathUtils { + + private RestPathUtils() { + } + + /** + * Build resource path from provided resource class. + *

        + * NOTE that this method should not be used for sub-resources, because their {@link jakarta.ws.rs.Path} annotation + * is ignored (lookup method path used instead). + * + * @param resource resource class to build a path for + * @return path for provided resource (or sub resource) with preserved path parameters placeholders + * @throws java.lang.IllegalStateException if {@link jakarta.ws.rs.Path} annotation not found in resource class + * hierarchy + */ + public static String getResourcePath(final Class resource) { + return getResourcePath(null, resource); + } + + /** + * Build resource path from provided resource class. + *

        + * NOTE that this method should not be used for sub-resources, because their {@link jakarta.ws.rs.Path} annotation + * is ignored (lookup method path used instead). + * + * @param basePath optional base path (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @param resource resource class to build a path for + * @return path for provided resource (or sub resource) with preserved path parameters placeholders + * @throws java.lang.IllegalStateException if {@link jakarta.ws.rs.Path} annotation not found in resource class + * hierarchy + */ + public static String getResourcePath(final @Nullable String basePath, final Class resource, + final Object... args) { + return buildPath(basePath, resource, args).buildTemplate(); + } + + /** + * Build resource path from provided resource class. + * + * @param resource resource class to build a path for + * @param resource type + * @return builder to specify path or query params + * @throws java.lang.IllegalStateException if {@link jakarta.ws.rs.Path} annotation not found in resource class + * hierarchy + */ + public static RestPathBuilder buildPath(final Class resource) { + return buildPath(null, resource); + } + + /** + * Build resource path from provided resource classe. + * + * @param basePath optional base path (could contain String.format() placeholders: %s) + * @param args variables for path placeholders (String.format() arguments) + * @param resource resource class to build a path for + * @param resource type + * @return builder to specify path or query params + * @throws java.lang.IllegalStateException if {@link jakarta.ws.rs.Path} annotation not found in resource class + * hierarchy + */ + public static RestPathBuilder buildPath(final @Nullable String basePath, final Class resource, + final Object... args) { + return new RestPathBuilder<>(basePath != null ? String.format(basePath, args) : null, resource, false); + } + + /** + * Build resource path from provided sub-resource class. + * IMPORTANT: the difference with {@link #buildPath(String, Class, Object...)} is that {@link jakarta.ws.rs.Path} + * annotation value is ignored for sub-resources (instead, the path from lookup method is used, which must be + * specified manually). + * + * @param basePath sub resource mapping from locator method (could contain String.format() placeholders: %s) + * @param subResource resource sub-resource class to build a path for + * @param args variables for path placeholders (String.format() arguments) + * @return builder to specify path or query params + * @param sub-resource type + */ + public static RestPathBuilder buildSubResourcePath(final @Nullable String basePath, + final Class subResource, final Object... args) { + return new RestPathBuilder<>(basePath != null ? String.format(basePath, args) : null, subResource, true); + } +} diff --git a/dropwizard-guicey/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup b/dropwizard-guicey/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup new file mode 100644 index 000000000..e64644da3 --- /dev/null +++ b/dropwizard-guicey/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup @@ -0,0 +1,6 @@ +ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClientFieldsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClientFieldsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.log.LogFieldsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.RestStubFieldsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubFieldsSupport +ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockFieldsSupport \ No newline at end of file diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractPlatformTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractPlatformTest.java new file mode 100644 index 000000000..5aeb3bb34 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractPlatformTest.java @@ -0,0 +1,83 @@ +package ru.vyarus.dropwizard.guice; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.testkit.engine.EngineTestKit; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; +import uk.org.webcompere.systemstubs.stream.SystemOut; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +@ExtendWith(SystemStubsExtension.class) +public abstract class AbstractPlatformTest { + + @SystemStub + SystemOut out; + + Throwable th; + + protected Throwable runFailed(Class... tests) { + run(tests); + return Preconditions.checkNotNull(th, "Exception expected, but was not thrown"); + } + + protected String runSuccess(Class... tests) { + String res = run(tests); + Preconditions.checkState(th == null, "Exception was not expected, but thrown: %s", th != null ? th.getMessage() : null); + return res; + } + + protected String run(Class... tests) { + th = null; + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(Arrays.stream(tests) + .map(DiscoverySelectors::selectClass) + .toArray(DiscoverySelector[]::new)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + th = err; + }); + + return doClean(out.getText()); + } + + + protected String doClean(String out) { + // use err because out is redirected + System.err.println(out); + + String res = clean(out.replace("\r", "")); + + if (!res.isEmpty()) { + System.err.println("Cleared -------------------------------------------------------"); + System.err.println(res); + System.err.println("---------------------------------------------------------------"); + } + return res; + + } + + protected abstract String clean(String out); + + protected String unifyMs(String out) { + return out.replaceAll("\\d+(\\.\\d+)? ms( +)?", "111 ms "); + } + + protected String unifyLambdas(String out) { + return out.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") + // jdk 21 + .replaceAll("\\$\\$Lambda/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy new file mode 100644 index 000000000..f30a3ae29 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice + +import ch.qos.logback.classic.Level +import com.codahale.metrics.health.HealthCheckRegistry +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import io.dropwizard.jetty.setup.ServletEnvironment +import io.dropwizard.lifecycle.setup.LifecycleEnvironment +import io.dropwizard.logging.common.BootstrapLogging +import io.dropwizard.logging.common.LoggingUtil +import io.dropwizard.core.setup.AdminEnvironment +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState +import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle +import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle +import ru.vyarus.dropwizard.guice.test.EnableHook +import spock.lang.Specification + +import jakarta.servlet.FilterRegistration +import jakarta.servlet.ServletRegistration + +/** + * Base class for tests. + * + * @author Vyacheslav Rusakov + * @since 31.08.2014 + */ +abstract class AbstractTest extends Specification { + + static { + BootstrapLogging.bootstrap(Level.DEBUG); // bootstrap set threshold filter! + LoggingUtil.getLoggerContext().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).setLevel(Level.WARN) + LoggingUtil.getLoggerContext().getLogger("ru.vyarus.dropwizard.guice").setLevel(Level.INFO) + Locale.setDefault(Locale.ENGLISH) + } + + // common guicey extra extensions used for all tests + @EnableHook + public static GuiceyConfigurationHook TEST_HOOK = { it.bundles(new HK2DebugBundle(), new GuiceRestrictedConfigBundle()) } + + void cleanupSpec() { + // some tests are intentionally failing so be sure to remove stale applications + SharedConfigurationState.clear() + System.clearProperty(PropertyBundleLookup.BUNDLES_PROPERTY) + } + + Environment mockEnvironment() { + def environment = Mock(Environment) + environment.jersey() >> Mock(JerseyEnvironment) + environment.servlets() >> Mock(ServletEnvironment) + environment.servlets().addFilter(*_) >> Mock(FilterRegistration.Dynamic) + environment.servlets().addServlet(*_) >> Mock(ServletRegistration.Dynamic) + environment.getApplicationContext() >> Mock(MutableServletContextHandler) + environment.admin() >> Mock(AdminEnvironment) + environment.admin().addFilter(*_) >> Mock(FilterRegistration.Dynamic) + environment.admin().addServlet(*_) >> Mock(ServletRegistration.Dynamic) + environment.getAdminContext() >> Mock(MutableServletContextHandler) + environment.lifecycle() >> Mock(LifecycleEnvironment) + environment.healthChecks() >> Mock(HealthCheckRegistry) + return environment + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ApplicationInjectionsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ApplicationInjectionsTest.java new file mode 100644 index 000000000..a22b5aa2f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ApplicationInjectionsTest.java @@ -0,0 +1,50 @@ +package ru.vyarus.dropwizard.guice; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 18.10.2025 + */ +@TestGuiceyApp(ApplicationInjectionsTest.App.class) +public class ApplicationInjectionsTest { + + @Inject Service service; + + @Test + void testApplicationInjection() { + Assertions.assertThat(service.called).isTrue(); + } + + public static class App extends Application { + + @Inject Service service; + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + service.doSomething(); + } + } + + @Singleton + public static class Service { + boolean called; + + public void doSomething() { + called = true; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoConfigShortcutTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoConfigShortcutTest.groovy new file mode 100644 index 000000000..1d27a0816 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoConfigShortcutTest.groovy @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice + +import com.google.inject.Inject +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller +import ru.vyarus.dropwizard.guice.support.auto2.AutoScanApp2 +import ru.vyarus.dropwizard.guice.support.auto2.SampleResource +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 29.12.2022 + */ +@TestGuiceyApp(AutoScanApp2) +class AutoConfigShortcutTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check auto scan configuration"() { + + when: "application started" + + then: "app package substituted" + info.options.getValue(GuiceyOptions.ScanPackages) == [AutoScanApp2.package.name] + + then: "resource found" + info.getExtensions(ResourceInstaller) == [SampleResource] + + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy index 6b748ad02..35993a4aa 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/AutoScanModeTest.groovy @@ -4,9 +4,9 @@ import com.google.inject.Inject import com.google.inject.Injector import com.google.inject.Key import com.google.inject.TypeLiteral -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.LifeCycleInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller @@ -75,7 +75,7 @@ class AutoScanModeTest extends AbstractTest { injector.getExistingBinding(Key.get(DummyLifeCycle)) then: "jersey provider found" - info.getExtensions(JerseyProviderInstaller) as Set == [DummyExceptionMapper, DummyJerseyProvider, DummyOtherProvider] as Set + info.getExtensions(JerseyProviderInstaller) as Set == [DummyExceptionMapper, DummyJerseyProvider, DummyOtherProvider, DummyModelProcessor] as Set injector.getExistingBinding(Key.get(DummyExceptionMapper)) injector.getExistingBinding(Key.get(DummyJerseyProvider)) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy index 55dabeaa5..d62d383a1 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/BridgeActivationTest.groovy @@ -1,19 +1,19 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.debug.ConfigurationDiagnostic import ru.vyarus.dropwizard.guice.debug.report.option.OptionsConfig import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy index b278ca43e..2d106a6f4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CommandInstantiationFailTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification @@ -16,11 +16,10 @@ class CommandInstantiationFailTest extends Specification { def "Check not instantiatable command"() { when: "run app" - TestSupport.runCoreApp(App, null) + TestSupport.runCoreApp(App) then: "error" def ex = thrown(IllegalStateException) - ex.message.startsWith( - "Failed to instantiate command") + ex.message.startsWith("Failed to instantiate command") } static class App extends Application { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/CommandTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CommandTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/CommandTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CommandTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy index a10f20880..b3b7cf80b 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CoreInstallersDisableTest.groovy @@ -1,14 +1,14 @@ package ru.vyarus.dropwizard.guice -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/CustomInstallerTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CustomInstallerTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/CustomInstallerTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CustomInstallerTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/CustomModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CustomModuleTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/CustomModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/CustomModuleTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy index 37473c25d..25e0803aa 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleInHkFistModeTest.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Injector import com.google.inject.ProvisionException -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.hk2.api.MultiException import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.internal.inject.InjectionManager @@ -18,12 +18,12 @@ import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -37,7 +37,7 @@ class DebugBundleInHkFistModeTest extends AbstractTest { @Inject Injector injector @Inject - javax.inject.Provider locator; + jakarta.inject.Provider locator; def "Check correct scopes"() { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy index fea6888ee..b263307eb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DebugBundleTest.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Injector import com.google.inject.ProvisionException -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.hk2.api.MultiException import org.glassfish.jersey.internal.inject.AbstractBinder import org.glassfish.jersey.internal.inject.InjectionManager @@ -18,12 +18,12 @@ import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -37,7 +37,7 @@ class DebugBundleTest extends AbstractTest { @Inject Injector injector @Inject - javax.inject.Provider locator; + jakarta.inject.Provider locator; def "Check correct scopes"() { diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DelayedConfigurationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DelayedConfigurationTest.java new file mode 100644 index 000000000..b3e9ff8b4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DelayedConfigurationTest.java @@ -0,0 +1,100 @@ +package ru.vyarus.dropwizard.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import io.dropwizard.lifecycle.Managed; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 13.03.2025 + */ +public class DelayedConfigurationTest extends AbstractPlatformTest { + + @Test + void testDelayedConfiguration() { + String out = runSuccess(Test1.class); + org.assertj.core.api.Assertions.assertThat(out) + .contains("Mng (r.v.d.g.DelayedConfigurationTest)"); + } + + @Test + void testDelayedConfigurationDisable() { + String out = runSuccess(Test2.class); + org.assertj.core.api.Assertions.assertThat(out) + .doesNotContain("Mng (r.v.d.g.DelayedConfigurationTest)"); + } + + + @TestGuiceyApp(Test1.App.class) + @Disabled + public static class Test1 { + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testDelayedConfig() { + Assertions.assertTrue(info.getExtensions().contains(Mng.class)); + Assertions.assertTrue(info.getModules().contains(Mod.class)); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .whenConfigurationReady(environment -> + environment + .extensions(Mng.class) + .modules(new Mod())) + .build(); + } + } + } + + @TestGuiceyApp(Test2.App.class) + @Disabled + public static class Test2 { + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testDelayedConfig() { + Assertions.assertFalse(info.getExtensions().contains(Mng.class)); + Assertions.assertTrue(info.getExtensionsDisabled().contains(Mng.class)); + Assertions.assertFalse(info.getModules().contains(Mod.class)); + Assertions.assertTrue(info.getModulesDisabled().contains(Mod.class)); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .whenConfigurationReady(environment -> + environment + .extensions(Mng.class) + .disableExtensions(Mng.class) + .modules(new Mod()) + .disableModules(Mod.class)) + .build(); + } + } + } + + + + public static class Mng implements Managed {} + + public static class Mod extends AbstractModule {} + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy index dff403398..81ae490c3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DevStageTest.groovy @@ -1,15 +1,15 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Stage -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy index 0a0adb76c..69b974700 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/DisabledFeatureTest.groovy @@ -3,7 +3,7 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Inject import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.setup.Bootstrap import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.TaskInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy index f67544b06..6960ed7cf 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ExtensionRecognitionFailTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.support.feature.DummyPlugin1 import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification @@ -17,7 +17,7 @@ class ExtensionRecognitionFailTest extends Specification { def "Check no installer for extension"() { when: "run app" - TestSupport.runCoreApp(App, null) + TestSupport.runCoreApp(App) then: "error" def ex = thrown(IllegalStateException) ex.message.startsWith( diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy index f16847cc1..2244c208f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/InstallerRegistrationFailTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification @@ -17,7 +17,7 @@ class InstallerRegistrationFailTest extends Specification { def "Check no installer for extension"() { when: "run app" - TestSupport.runCoreApp(App, null) + TestSupport.runCoreApp(App) then: "error" def ex = thrown(IllegalStateException) ex.message.startsWith( diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ListenersTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ListenersTest.java new file mode 100644 index 000000000..2d0ec5d44 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ListenersTest.java @@ -0,0 +1,107 @@ +package ru.vyarus.dropwizard.guice; + +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.util.component.LifeCycle; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ + +public class ListenersTest extends AbstractPlatformTest { + + @Test + void testListenerMethods() { + runSuccess(Test1.class); + Assertions.assertThat(Test1.actions).isEqualTo(Arrays.asList( + "whenConfigurationReady", + "onGuiceyStartup", + "lifeCycleStarting", + "INITIALIZATION_START", + "INITIALIZATION_APP_FINISHED", + "INITIALIZATION_FINISHED", + "onApplicationStartup", + "lifeCycleStarted", + "listenServer", + "lifeCycleStopping", + "DESTROY_FINISHED", + "onApplicationShutdown", + "lifeCycleStopped" + )); + } + + @TestDropwizardApp(Test1.App.class) + @Disabled + public static class Test1 { + static List actions = new ArrayList<>(); + + @Test + void test() { + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .whenConfigurationReady(environment -> actions.add("whenConfigurationReady")) + .onGuiceyStartup((config, env, injector) -> + actions.add("onGuiceyStartup")) + .onApplicationStartup(context -> actions.add("onApplicationStartup")) + .onApplicationShutdown(context -> actions.add("onApplicationShutdown")) + .listenJetty(new LifeCycle.Listener() { + @Override + public void lifeCycleStarting(LifeCycle event) { + actions.add("lifeCycleStarting"); + } + + @Override + public void lifeCycleStarted(LifeCycle event) { + actions.add("lifeCycleStarted"); + } + + @Override + public void lifeCycleStopping(LifeCycle event) { + actions.add("lifeCycleStopping"); + } + + @Override + public void lifeCycleStopped(LifeCycle event) { + actions.add("lifeCycleStopped"); + } + }) + .listenJersey(new ApplicationEventListener() { + @Override + public void onEvent(ApplicationEvent event) { + actions.add(event.getType().name()); + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return null; + } + }) + .listenServer(server -> actions.add("listenServer")) + .build(); + } + } + } + + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy index d11ab7dbc..e2a922cc8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/ManualModeTest.groovy @@ -3,9 +3,9 @@ package ru.vyarus.dropwizard.guice import com.google.inject.Inject import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.TaskInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy index b0b046a31..78b27c1c2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/OptionsLookupTest.groovy @@ -1,14 +1,14 @@ package ru.vyarus.dropwizard.guice import com.google.common.collect.ImmutableMap -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy new file mode 100644 index 000000000..edb0be382 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy @@ -0,0 +1,160 @@ +package ru.vyarus.dropwizard.guice.bundles + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import io.dropwizard.lifecycle.ServerLifecycleListener +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.util.component.LifeCycle +import org.glassfish.jersey.server.monitoring.ApplicationEvent +import org.glassfish.jersey.server.monitoring.ApplicationEventListener +import org.glassfish.jersey.server.monitoring.RequestEvent +import org.glassfish.jersey.server.monitoring.RequestEventListener +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import ru.vyarus.dropwizard.guice.AbstractPlatformTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 18.09.2019 + */ +class EnvironmentListenersShortcutsTest extends AbstractPlatformTest { + + @Test + void testListeners() { + + when: "run test" + run(Test1) + when: "listeners called" + Mng.start + Mng.stop + LListener.start + LListener.stop + JListener.called + SListener.start + Bundle.onGuiceyStartup + Bundle.onStartup + Bundle.onShutdown + } + + @TestDropwizardApp(App) + @Disabled + static class Test1 { + + @Test + void test() { + Assertions.assertTrue(Mng.start) + Assertions.assertTrue(LListener.start) + Assertions.assertTrue(JListener.called) + Assertions.assertTrue(SListener.start) + Assertions.assertTrue(Bundle.onGuiceyStartup) + Assertions.assertTrue(Bundle.onStartup) + } + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new Bundle()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Bundle implements GuiceyBundle { + static boolean onStartup + static boolean onShutdown + static boolean onGuiceyStartup + + @Override + void run(GuiceyEnvironment environment) { + environment.manage(new Mng()) + environment.listenJetty(new LListener()) + environment.listenJersey(new JListener()) + environment.listenServer(new SListener()) + environment.onGuiceyStartup({ cfg, env, inj -> + onGuiceyStartup = true + assert cfg != null + assert env != null + assert inj != null + }) + environment.onApplicationStartup({ inj -> + onStartup = true + assert inj != null + }) + environment.onApplicationShutdown ({ inj -> + onShutdown = true + assert inj != null + }) + } + } + + static class Mng implements Managed { + static boolean start + static boolean stop + + @Override + void start() throws Exception { + start = true + } + + @Override + void stop() throws Exception { + stop = true + } + } + + static class LListener implements LifeCycle.Listener { + static boolean start + static boolean stop + + @Override + void lifeCycleStarted(LifeCycle event) { + start = true + } + + @Override + void lifeCycleStopped(LifeCycle event) { + stop = true + } + } + + static class SListener implements ServerLifecycleListener { + static boolean start + + @Override + void serverStarted(Server server) { + start = true + } + } + + static class JListener implements ApplicationEventListener { + static boolean called + + @Override + void onEvent(ApplicationEvent event) { + called = true + } + + @Override + RequestEventListener onRequest(RequestEvent requestEvent) { + return null + } + } + + @Override + protected String clean(String out) { + return out + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy similarity index 71% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy index ce73c84fd..1576a720e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersWuthCommandTest.groovy @@ -1,16 +1,17 @@ package ru.vyarus.dropwizard.guice.bundles -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.cli.EnvironmentCommand -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import io.dropwizard.testing.junit.DropwizardAppRule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.cli.EnvironmentCommand +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import net.sourceforge.argparse4j.inf.Namespace import org.eclipse.jetty.util.component.LifeCycle import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.dropwizard.guice.test.util.RunResult import spock.lang.Specification /** @@ -22,27 +23,30 @@ class EnvironmentListenersWuthCommandTest extends Specification { def "Check lifecycle under command"() { when: "run command" - new App().run(['test'] as String[]) + App app = new App() + app.run(['test'] as String[]) then: "listener not called" - !App.lifecycleStarted - App.guiceyStarted - !App.serverStarted + !app.lifecycleStarted + app.guiceyStarted + !app.serverStarted + !app.appShutdown when: "run application normally" - def rule = new DropwizardAppRule<>(App) - rule.before() - rule.after() + RunResult res = TestSupport.runWebApp(App) then: "listener called" - App.lifecycleStarted - App.guiceyStarted - App.serverStarted + App app1 = res.application + app1.lifecycleStarted + app1.guiceyStarted + app1.serverStarted + app1.appShutdown } static class App extends Application { - static boolean lifecycleStarted - static boolean guiceyStarted - static boolean serverStarted + boolean lifecycleStarted + boolean guiceyStarted + boolean serverStarted + boolean appShutdown @Override void initialize(Bootstrap bootstrap) { @@ -60,6 +64,7 @@ class EnvironmentListenersWuthCommandTest extends Specification { }) .onGuiceyStartup({ a, b, c -> guiceyStarted = true }) .onApplicationStartup({ serverStarted = true }) + .onApplicationShutdown {appShutdown = true} } }) .build()) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy index ac7633532..fc0557525 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentMethodsTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.bundles import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap @@ -14,8 +14,8 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.core.FeatureContext +import jakarta.inject.Inject +import jakarta.ws.rs.core.FeatureContext /** * @author Vyacheslav Rusakov @@ -81,7 +81,7 @@ class EnvironmentMethodsTest extends Specification { @EagerSingleton static class Ext {} - static class Feature implements javax.ws.rs.core.Feature { + static class Feature implements jakarta.ws.rs.core.Feature { static int called @Override @@ -91,7 +91,7 @@ class EnvironmentMethodsTest extends Specification { } } - static class Feature2 implements javax.ws.rs.core.Feature { + static class Feature2 implements jakarta.ws.rs.core.Feature { static int called @Override diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ExtensionRegistrationInRunPhase.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ExtensionRegistrationInRunPhase.java new file mode 100644 index 000000000..dd14ec5c2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ExtensionRegistrationInRunPhase.java @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.bundles; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 13.03.2025 + */ +@TestGuiceyApp(ExtensionRegistrationInRunPhase.App.class) +public class ExtensionRegistrationInRunPhase { + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testExtensionRegistrationInRunPhase() { + final ExtensionItemInfo ext = info.getInfo(ManagedBean.class); + Assertions.assertNotNull(ext); + } + + public static class App extends DefaultTestApp { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new GuiceyBundle() { + @Override + public void run(GuiceyEnvironment environment) throws Exception { + environment.extensions(ManagedBean.class); + } + }) + .printLifecyclePhasesDetailed() + .build()); + } + } + + public static class ManagedBean implements Managed {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy index 4485ae88d..fac051870 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/Hk2DebugEnableOptionTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.bundles -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.VoidBundleLookup import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/InitExceptionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/InitExceptionTest.java new file mode 100644 index 000000000..373b15500 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/InitExceptionTest.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.bundles; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 21.03.2025 + */ +public class InitExceptionTest { + + @Test + void testCheckedException() { + final IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> { + TestSupport.build(DefaultTestApp.class) + .hooks(builder -> builder.bundles(new GuiceyBundle() { + @Override + public void initialize(GuiceyBootstrap bootstrap) throws Exception { + throw new IOException("error"); + } + })) + .runCore(); + }); + Assertions.assertEquals("Guicey bundle initialization failed", ex.getMessage()); + } + + @Test + void testRuntimeException() { + final RuntimeException ex = Assertions.assertThrows(RuntimeException.class, () -> { + TestSupport.build(DefaultTestApp.class) + .hooks(builder -> builder.bundles(new GuiceyBundle() { + @Override + public void initialize(GuiceyBootstrap bootstrap) throws Exception { + throw new RuntimeException("error"); + } + })) + .runCore(); + }); + Assertions.assertEquals("error", ex.getMessage()); + } + +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy index 94f5c8e97..48aa86b43 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/ListenersCallWithinCommandTest.groovy @@ -1,14 +1,14 @@ package ru.vyarus.dropwizard.guice.bundles -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.cli.EnvironmentCommand +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.cli.EnvironmentCommand +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import io.dropwizard.lifecycle.Managed -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import io.dropwizard.testing.junit.DropwizardAppRule import net.sourceforge.argparse4j.inf.Namespace import org.eclipse.jetty.util.component.LifeCycle +import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification /** @@ -28,9 +28,7 @@ class ListenersCallWithinCommandTest extends Specification { !Mng.stopped when: "run application normally" - def rule = new DropwizardAppRule<>(App) - rule.before() - rule.after() + TestSupport.runWebApp(App, null) then: "listener called" Listener.called == ['starting', 'started', 'stopping', 'stopped'] and: "managed called" @@ -48,7 +46,7 @@ class ListenersCallWithinCommandTest extends Specification { @Override void run(Configuration configuration, Environment environment) throws Exception { environment.lifecycle().manage(new Mng()) - environment.lifecycle().addLifeCycleListener(new Listener()) + environment.lifecycle().addEventListener(new Listener()) } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/BootstrapMethodsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/BootstrapMethodsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/BootstrapMethodsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/BootstrapMethodsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy index 4b254de14..555cfa183 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/bootstrap/GBootstrapApplication.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.bundles.bootstrap import com.google.inject.AbstractModule import com.google.inject.name.Names -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap @@ -59,6 +59,7 @@ class GBootstrapApplication extends Application { assert environment.configuration() != null assert environment.environment() != null assert environment.configurationTree() != null + assert environment.bootstrap() != null assert environment.application() != null assert environment.option(GuiceyOptions.UseCoreInstallers) } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/DefaultLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/DefaultLookupTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/DefaultLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/DefaultLookupTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/PropertyLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/PropertyLookupTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/PropertyLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/PropertyLookupTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/ServiceLoaderLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/ServiceLoaderLookupTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/ServiceLoaderLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/ServiceLoaderLookupTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/VoidLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/VoidLookupTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/VoidLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/lookup/VoidLookupTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy index 653fc6160..26c3dd261 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.bundles.manual -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.feature.DummyManaged diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesRegistrationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesRegistrationTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesRegistrationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/manual/ManualBundlesRegistrationTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy index d0308f53a..c37b537b2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPrevention2Test.groovy @@ -13,7 +13,7 @@ class LoopPrevention2Test extends AbstractTest { def "Check loop prevention"() { when: "starting app" - TestSupport.runCoreApp(LoopApp2, null) + TestSupport.runCoreApp(LoopApp2) then: "startup failed" def ex = thrown(IllegalStateException) ex.getMessage() == "Bundles registration loop detected: ( LoopBundle1 -> LoopBundle2 ) -> LoopBundle1 ..." diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy index 8df5f4385..4a5baa614 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/LoopPreventionTest.groovy @@ -13,7 +13,7 @@ class LoopPreventionTest extends AbstractTest { def "Check bundle loop prevention"() { when: "starting app" - TestSupport.runCoreApp(LoopApplication, null) + TestSupport.runCoreApp(LoopApplication) then: "startup failed" def ex = thrown(IllegalStateException) ex.getMessage() == "Bundles registration loop detected: ( LoopBundle ) -> LoopBundle ..." diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesInitTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesInitTest.groovy new file mode 100644 index 000000000..cb50370b9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesInitTest.groovy @@ -0,0 +1,85 @@ +package ru.vyarus.dropwizard.guice.bundles.transitive + +import com.google.inject.Inject +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.installer.CoreInstallersBundle +import ru.vyarus.dropwizard.guice.module.installer.WebInstallersBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment +import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 18.03.2025 + */ +@TestGuiceyApp(App) +class TransitiveBundlesInitTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check transitive installation order"() { + + expect: + initOrder == ["Last", "Middle", "Root"] + runOrder == ["Last", "Middle", "Root"] + info.getGuiceyBundlesInInitOrder() == [Last, Middle, Root, HK2DebugBundle, GuiceRestrictedConfigBundle, WebInstallersBundle, CoreInstallersBundle] + } + + static List initOrder = new ArrayList<>() + static List runOrder = new ArrayList<>() + + static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .bundles(new Root()) + .build() + } + } + + static class Root implements GuiceyBundle { + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.bundles(new Middle()) + initOrder.add("Root") + } + + @Override + void run(GuiceyEnvironment environment) throws Exception { + runOrder.add("Root") + } + } + + static class Middle implements GuiceyBundle { + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.bundles(new Last()) + initOrder.add("Middle") + } + + @Override + void run(GuiceyEnvironment environment) throws Exception { + runOrder.add("Middle") + } + } + + static class Last implements GuiceyBundle { + + @Override + void initialize(GuiceyBootstrap bootstrap) { + initOrder.add("Last") + } + + @Override + void run(GuiceyEnvironment environment) throws Exception { + runOrder.add("Last") + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy index e86c0345d..3d79e8b28 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/TransitiveBundlesTest.groovy @@ -8,7 +8,7 @@ import ru.vyarus.dropwizard.guice.bundles.transitive.support.TransitiveBundlesAp import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle1.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle1.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle3.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle3.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle3.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/Bundle3.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy index 47f8cf6d8..69e894c86 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/TransitiveBundlesApp.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.bundles.transitive.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy index 76824909f..4f7551917 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopApplication.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.bundles.transitive.support.loop -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopBundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop/LoopBundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy index 827605a7c..effc6ee4d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopApp2.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.bundles.transitive.support.loop2 -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle1.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle1.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/transitive/support/loop2/LoopBundle2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java index 3f62b6c7b..2eec24d28 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnnModuleApp.java @@ -2,10 +2,10 @@ import com.google.inject.AbstractModule; import com.google.inject.BindingAnnotation; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import ru.vyarus.dropwizard.guice.GuiceBundle; import java.lang.annotation.Retention; diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy index 4a92181ca..1eaf55da7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/analysis/AnonymousModuleAnalysisTest.groovy @@ -7,7 +7,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy index 63888b3e8..96930649e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.cases.dwaware -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/dwaware/DwAwareModuleTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy index f52e7bd89..7afc5947e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HKScopeTest.groovy @@ -5,9 +5,9 @@ import com.google.inject.Key import com.google.inject.Scope import com.google.inject.Scopes import com.google.inject.spi.DefaultBindingScopingVisitor -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.hk2.api.Descriptor import org.glassfish.hk2.api.Filter import org.glassfish.hk2.api.ServiceLocator @@ -22,12 +22,12 @@ import ru.vyarus.dropwizard.guice.module.jersey.debug.service.ContextDebugServic import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.inject.Provider -import javax.inject.Singleton -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.ParamConverterProvider -import javax.ws.rs.ext.Providers +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.inject.Singleton +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.ParamConverterProvider +import jakarta.ws.rs.ext.Providers import java.lang.annotation.Annotation /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy index 66d095e0c..16338997a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/HkFirstModeScopeTest.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.cases.hkscope -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.InjectionResolver import ru.vyarus.dropwizard.guice.AbstractTest @@ -11,11 +11,11 @@ import ru.vyarus.dropwizard.guice.module.jersey.debug.service.ContextDebugServic import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.inject.Provider -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.ParamConverterProvider -import javax.ws.rs.ext.Providers +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.ParamConverterProvider +import jakarta.ws.rs.ext.Providers /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/Ann.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/Ann.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/Ann.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/Ann.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy index 55dcb8fa1..ca6670690 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceApplicationEventListener.groovy @@ -6,7 +6,7 @@ import org.glassfish.jersey.server.monitoring.RequestEvent import org.glassfish.jersey.server.monitoring.RequestEventListener import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy similarity index 73% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy index 497a421ad..2cf85f1a9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerRequestFilter.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerRequestFilter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.container.ContainerRequestFilter +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy similarity index 68% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy index a4b9aef40..b96bb6731 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContainerResponseFilter.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerResponseContext -import javax.ws.rs.container.ContainerResponseFilter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.container.ContainerResponseContext +import jakarta.ws.rs.container.ContainerResponseFilter +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy index ac791dbe8..ba2ae7148 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceContextResolver.groovy @@ -2,8 +2,8 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.ext.ContextResolver -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ContextResolver +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy similarity index 69% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy index 8fa262a2d..6302c12f4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceDynamicFeature.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.container.DynamicFeature -import javax.ws.rs.container.ResourceInfo -import javax.ws.rs.core.FeatureContext -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.DynamicFeature +import jakarta.ws.rs.container.ResourceInfo +import jakarta.ws.rs.core.FeatureContext +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy index f46a1f934..aad8ea607 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceExceptionMapper.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy index 8458dacd0..cd5dbbb17 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceInjectionResolver.groovy @@ -4,7 +4,7 @@ import org.glassfish.jersey.internal.inject.Injectee import org.glassfish.jersey.internal.inject.InjectionResolver import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy index 02f0b4d54..c841ccb53 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyReader.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.MultivaluedMap -import javax.ws.rs.ext.MessageBodyReader -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.MultivaluedMap +import jakarta.ws.rs.ext.MessageBodyReader +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy similarity index 82% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy index bfbc1a13f..7daebcc1a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceMessageBodyWriter.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.MultivaluedMap -import javax.ws.rs.ext.MessageBodyWriter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.MultivaluedMap +import jakarta.ws.rs.ext.MessageBodyWriter +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy index 14f8b2c87..a7fc1fa0e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceParamConverterProvider.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.ext.ParamConverter -import javax.ws.rs.ext.ParamConverterProvider -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ParamConverter +import jakarta.ws.rs.ext.ParamConverterProvider +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation import java.lang.reflect.Type diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy similarity index 71% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy index c70d64e74..d62336b7e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceReaderInterceptor.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.ext.Provider -import javax.ws.rs.ext.ReaderInterceptor -import javax.ws.rs.ext.ReaderInterceptorContext +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ReaderInterceptor +import jakarta.ws.rs.ext.ReaderInterceptorContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy index d18f0a910..7318169ff 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceResource.groovy @@ -2,8 +2,8 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.GET -import javax.ws.rs.Path +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy index 27287bb6d..c2899a817 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceValueParamProvider.groovy @@ -5,7 +5,7 @@ import org.glassfish.jersey.server.model.Parameter import org.glassfish.jersey.server.spi.internal.ValueParamProvider import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy index 3ebaffcc2..32c4ada88 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/GuiceWriterInterceptor.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.GuiceManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.ext.Provider -import javax.ws.rs.ext.WriterInterceptor -import javax.ws.rs.ext.WriterInterceptorContext +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.ext.Provider +import jakarta.ws.rs.ext.WriterInterceptor +import jakarta.ws.rs.ext.WriterInterceptorContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy index 091e8c045..9ab0e1a55 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKApplicationEventListener.groovy @@ -6,7 +6,7 @@ import org.glassfish.jersey.server.monitoring.RequestEvent import org.glassfish.jersey.server.monitoring.RequestEventListener import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy similarity index 73% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy index 1394ffa03..18b777d7f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerRequestFilter.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerRequestFilter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.container.ContainerRequestFilter +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy similarity index 68% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy index 49f29f439..0c88b58e2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContainerResponseFilter.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerResponseContext -import javax.ws.rs.container.ContainerResponseFilter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.container.ContainerResponseContext +import jakarta.ws.rs.container.ContainerResponseFilter +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy index cced6f2cf..836946ed0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKContextResolver.groovy @@ -2,8 +2,8 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.ext.ContextResolver -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ContextResolver +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy similarity index 69% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy index 8bcda3bed..73e11665c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKDynamicFeature.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.container.DynamicFeature -import javax.ws.rs.container.ResourceInfo -import javax.ws.rs.core.FeatureContext -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.container.DynamicFeature +import jakarta.ws.rs.container.ResourceInfo +import jakarta.ws.rs.core.FeatureContext +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy index 5964da537..a9231069d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKExceptionMapper.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy index 2e8251c22..2af309f06 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKInjectionResolver.groovy @@ -5,7 +5,7 @@ import org.glassfish.jersey.internal.inject.InjectionResolver import ru.vyarus.dropwizard.guice.cases.hkscope.support.Ann import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy index 177b4283f..875d31a18 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyReader.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.MultivaluedMap -import javax.ws.rs.ext.MessageBodyReader -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.MultivaluedMap +import jakarta.ws.rs.ext.MessageBodyReader +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy similarity index 82% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy index 701c4cfa1..e9e65b048 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKMessageBodyWriter.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.MultivaluedMap -import javax.ws.rs.ext.MessageBodyWriter -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.MultivaluedMap +import jakarta.ws.rs.ext.MessageBodyWriter +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy index 0e7f0f77b..6fdbcaf7c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKParamConverterProvider.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.ext.ParamConverter -import javax.ws.rs.ext.ParamConverterProvider -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ParamConverter +import jakarta.ws.rs.ext.ParamConverterProvider +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation import java.lang.reflect.Type diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy similarity index 71% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy index 615c2e05b..b3e5ded6e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKReaderInterceptor.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.ext.Provider -import javax.ws.rs.ext.ReaderInterceptor -import javax.ws.rs.ext.ReaderInterceptorContext +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ReaderInterceptor +import jakarta.ws.rs.ext.ReaderInterceptorContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy index c2c5bac6f..2d0b96f20 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKResource.groovy @@ -2,8 +2,8 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.GET -import javax.ws.rs.Path +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy index 78faabefb..ea3a2d3ca 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKValueParamProvider.groovy @@ -6,7 +6,7 @@ import org.glassfish.jersey.server.model.Parameter import org.glassfish.jersey.server.spi.internal.ValueParamProvider import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy index 3e901b036..2d7c779c9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/hkscope/support/hk/HKWriterInterceptor.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.cases.hkscope.support.hk import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.WebApplicationException -import javax.ws.rs.ext.Provider -import javax.ws.rs.ext.WriterInterceptor -import javax.ws.rs.ext.WriterInterceptorContext +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.ext.Provider +import jakarta.ws.rs.ext.WriterInterceptor +import jakarta.ws.rs.ext.WriterInterceptorContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy index e26475a3e..e710de35f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/InterfaceResourceDefinitionTest.groovy @@ -9,9 +9,9 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstal import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.client.Client -import javax.ws.rs.client.ClientBuilder +import jakarta.inject.Inject +import jakarta.ws.rs.client.Client +import jakarta.ws.rs.client.ClientBuilder /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy index 143f16c13..c7a92dc08 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InterfaceResourceApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.cases.ifaceresource.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InvisibleResourceImpl.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InvisibleResourceImpl.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InvisibleResourceImpl.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/InvisibleResourceImpl.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy similarity index 67% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy index e86041c5d..02d75b34c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceContract.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.ifaceresource.support -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceImpl.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceImpl.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceImpl.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/ResourceImpl.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/SecondLevelResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/SecondLevelResource.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/SecondLevelResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/ifaceresource/support/SecondLevelResource.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy index e720dc780..2b39e2503 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/AbstractExceptionMapper.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.innercls -import javax.servlet.ServletException -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.servlet.ServletException +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy similarity index 78% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy index 744166b52..831fd3f9c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanApp.groovy @@ -1,10 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.innercls -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle -import ru.vyarus.dropwizard.guice.cases.dwaware.DwAwareModule import ru.vyarus.dropwizard.guice.support.TestConfiguration /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy index 3923e3a49..2a8aca028 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innercls/InnerClassScanTest.groovy @@ -5,7 +5,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.Jerse import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy index 9557f1241..8cf216e7e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/innernonstatic/InnerNonStaticTest.groovy @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.cases.innernonstatic -import io.dropwizard.Application -import io.dropwizard.Configuration +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration import io.dropwizard.lifecycle.Managed -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy index 4ea257f66..ba2e85dde 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ExceptionMapperOverrideTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers import ru.vyarus.dropwizard.guice.AbstractTest @@ -11,12 +11,12 @@ import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.ClientSupport import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy index da6a9e225..c1c125708 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProviderPriorityTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers import ru.vyarus.dropwizard.guice.AbstractTest @@ -12,13 +12,13 @@ import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions import ru.vyarus.dropwizard.guice.test.ClientSupport import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.annotation.Priority -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.annotation.Priority +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy index ac68e8f33..524c82e34 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/LegacyProvidersOrderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers import ru.vyarus.dropwizard.guice.AbstractTest @@ -12,12 +12,12 @@ import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions import ru.vyarus.dropwizard.guice.test.ClientSupport import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy index 09d991165..6d5f752fc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/ManualCustomTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.Custom import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers @@ -13,12 +13,12 @@ import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions import ru.vyarus.dropwizard.guice.test.ClientSupport import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy index 7974a572d..a79def942 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnQualifiedTest.groovy @@ -1,20 +1,20 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.annotation.Priority -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.annotation.Priority +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * The same test as {@link PriorityAnnTest}, but extensions will be qualified as Custom, which will use diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy index 2f6ee23bb..164d28bb9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/mapperoverride/PriorityAnnTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.mapperoverride -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.internal.inject.Providers import ru.vyarus.dropwizard.guice.AbstractTest @@ -11,11 +11,11 @@ import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.annotation.Priority -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.annotation.Priority +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy index 881a5dac8..ab66d6241 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiExtensionRegistrationTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.multicases -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.cases.multicases.support.Feature import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy index e15ccf2e7..5ee27c3e1 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiFeatureTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.cases.multicases -import io.dropwizard.Application -import io.dropwizard.Configuration +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration import io.dropwizard.lifecycle.Managed -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy index 81aaa6a09..3fef5d576 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/MultiInstallerRegistrationTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.multicases -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.cases.multicases.support.CustomInstaller import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy index 142154463..5fbbe4e79 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistration2Test.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.multicases -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.cases.multicases.support.CustomInstaller import ru.vyarus.dropwizard.guice.cases.multicases.support.Feature @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy index 93fd4f356..e0df733d0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/RepeatedRegistrationTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.cases.multicases -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.cases.multicases.support.CustomInstaller import ru.vyarus.dropwizard.guice.cases.multicases.support.Feature @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.installer.internal.ExtensionsHolder import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/CustomInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/CustomInstaller.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/CustomInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/CustomInstaller.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/Feature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/Feature.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/Feature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/multicases/support/Feature.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy index be5241115..9f58ed1ce 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/norequestscope/NoRequestScopeTest.groovy @@ -4,17 +4,17 @@ import com.google.inject.ProvisionException import com.google.inject.servlet.RequestScoped import com.google.inject.servlet.RequestScoper import com.google.inject.servlet.ServletScopes -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.util.BindModule import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.inject.Provider +import jakarta.inject.Inject +import jakarta.inject.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy index a4f194c46..bcb59442e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTask.groovy @@ -3,9 +3,9 @@ package ru.vyarus.dropwizard.guice.cases.taskreqscope import io.dropwizard.servlets.tasks.Task -import javax.inject.Inject -import javax.inject.Provider -import javax.servlet.http.HttpServletRequest +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.servlet.http.HttpServletRequest /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy index 47653a35c..5dd5decdb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.cases.taskreqscope -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.TaskInstaller import ru.vyarus.dropwizard.guice.support.TestConfiguration diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/cases/taskreqscope/RSAwareTaskTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy index d388c3d2c..a28a5ac3e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/AutoScanExtensionDisableTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy index 4f33ae831..2a0d22bb8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BasicConfigurationMappingTest.groovy @@ -1,12 +1,12 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.Injector -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.config.support.BasicApplication import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy index 9258c3581..7b68a5f2a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BootstrapProxyTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import net.sourceforge.argparse4j.inf.Namespace import net.sourceforge.argparse4j.inf.Subparser import ru.vyarus.dropwizard.guice.AbstractTest @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.bootstrap.BootstrapProxyFactory import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -48,7 +48,7 @@ class BootstrapProxyTest extends AbstractTest { } } - static class Command extends io.dropwizard.cli.Command { + static class Command extends io.dropwizard.core.cli.Command { Command() { super("sample", "") diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy index c01910129..b4de51c96 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/BundleLifecycleListenerTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy index e86e4d39e..919bafefa 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ComplexConfigurationMappingTest.groovy @@ -2,7 +2,7 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.config.support.ComplexConfigApp import ru.vyarus.dropwizard.guice.config.support.conf.* @@ -10,7 +10,7 @@ import ru.vyarus.dropwizard.guice.module.yaml.bind.Config import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Unroll -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy index 7db35298d..4fc8f0ed4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ConfigurationInfoEdgeCasesTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.ConfigItem import ru.vyarus.dropwizard.guice.module.context.ConfigurationInfo @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy new file mode 100644 index 000000000..eb0adce4e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy @@ -0,0 +1,207 @@ +package ru.vyarus.dropwizard.guice.config + +import io.dropwizard.core.Application +import io.dropwizard.core.ConfiguredBundle +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup +import ru.vyarus.dropwizard.guice.debug.ConfigurationDiagnostic +import ru.vyarus.dropwizard.guice.module.context.ConfigItem +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.module.context.Filters +import ru.vyarus.dropwizard.guice.module.context.info.ItemId +import ru.vyarus.dropwizard.guice.module.context.info.impl.* +import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.health.HealthCheckInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyFeatureInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller +import ru.vyarus.dropwizard.guice.module.installer.install.JerseyInstaller +import ru.vyarus.dropwizard.guice.module.installer.install.WebInstaller +import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner +import ru.vyarus.dropwizard.guice.module.jersey.Jersey2Module +import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle +import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 26.07.2016 + */ +class FiltersTest extends Specification { + + def "Check enabled filter"() { + + expect: "disabled item filtered" + !Filters.enabled().test(installer(JerseyFeatureInstaller) { + disabledBy.add(ItemId.from(Application)) + }) + + and: "enabled item filtered" + Filters.disabled().test(installer(JerseyFeatureInstaller) { + disabledBy.add(ItemId.from(Application)) + }) + + and: "not supported items enabled" + Filters.enabled().test(module(HK2DebugBundle.HK2DebugModule) {}) + + } + + def "Check disabledBy filter"() { + + expect: "matched check" + Filters.disabledBy(Application).test(installer(JerseyFeatureInstaller) { + disabledBy.add(ItemId.from(Application)) + }) + + and: "not matched check" + !Filters.disabledBy(Application).test(installer(JerseyFeatureInstaller) { + disabledBy.add(ItemId.from(ConfiguredBundle)) + }) + + } + + def "Check scan filter"() { + + expect: "from scan" + Filters.fromScan().test(installer(JerseyFeatureInstaller) { + registeredBy.add(ItemId.from(ClasspathScanner)) + }) + + and: "not from scan" + !Filters.fromScan().test(installer(JerseyFeatureInstaller)) + + and: "item not support scan" + !Filters.fromScan().test(module(HK2DebugBundle.HK2DebugModule)) + + } + + def "Check registrationScope filter"() { + + expect: "matched check" + Filters.registrationScope(ConfigScope.Application).test(installer(JerseyFeatureInstaller) { + countRegistrationAttempt(ItemId.from(Application)) + }) + + and: "not matched check" + !Filters.registrationScope(Application).test(installer(JerseyFeatureInstaller) { + countRegistrationAttempt(ItemId.from(ConfiguredBundle)) + }) + + } + + def "Check registeredBy filter"() { + + expect: "matched check" + Filters.registeredBy(ConfigScope.Application).test(installer(JerseyFeatureInstaller) { + registeredBy.add(ItemId.from(Application)) + }) + + and: "not matched check" + !Filters.registeredBy(Application).test(installer(JerseyFeatureInstaller) { + registeredBy.add(ItemId.from(ConfiguredBundle)) + }) + + } + + def "Check type filter"() { + + expect: "matched check" + Filters.type(ConfigItem.Installer).test(installer(JerseyFeatureInstaller)) + Filters.type(JerseyFeatureInstaller).test(installer(JerseyFeatureInstaller)) + + and: "not matched check" + !Filters.type(ConfigItem.Bundle).test(installer(JerseyFeatureInstaller)) + !Filters.type(HealthCheckInstaller).test(installer(JerseyFeatureInstaller)) + !Filters.type(HealthCheckInstaller).test(installer(JerseyFeatureInstaller)) + + } + + def "Check type filter shortcuts"() { + + expect: "bundles matched" + Filters.bundles().test(bundle(ConfigurationDiagnostic)) + Filters.bundles().test(dropwizardBundle(GuiceBundle)) + !Filters.bundles().test(installer(JerseyFeatureInstaller)) + + and: "guicey bundles matched" + Filters.guiceyBundles().test(bundle(ConfigurationDiagnostic)) + !Filters.guiceyBundles().test(dropwizardBundle(GuiceBundle)) + + and: "guicey dropwizard bundles matched" + !Filters.dropwizardBundles().test(bundle(ConfigurationDiagnostic)) + Filters.dropwizardBundles().test(dropwizardBundle(GuiceBundle)) + + and: "installers matched" + Filters.installers().test(installer(ResourceInstaller)) + !Filters.installers().test(bundle(ConfigurationDiagnostic)) + + and: "modules matched" + Filters.modules().test(module(Jersey2Module)) + !Filters.modules().test(bundle(ConfigurationDiagnostic)) + } + + def "Check web and jersey extensions"() { + + expect: + Filters.extensions().and(Filters.webExtension()).test(extension(HK2DebugFeature){ + installedBy = WebInstaller + }) + Filters.extensions().and(Filters.jerseyExtension()).test(extension(HK2DebugFeature) { + installedBy = JerseyInstaller + }) + !Filters.extensions().and(Filters.jerseyExtension()).test(extension(HK2DebugFeature) { + installedBy = ManagedInstaller + }) + } + + def "Check lookupBundles filter"() { + + expect: "matched check" + Filters.lookupBundles().test(bundle(ConfigurationDiagnostic) { + registeredBy.add(ItemId.from(GuiceyBundleLookup)) + }) + + and: "not matched check" + !Filters.lookupBundles().test(bundle(ConfigurationDiagnostic)) + + } + + def "Check installedBy filter"() { + + expect: "matched check" + Filters.installedBy(JerseyFeatureInstaller).test(extension(HK2DebugFeature) { + installedBy = JerseyFeatureInstaller + }) + + and: "not matched check" + !Filters.installedBy(JerseyFeatureInstaller).test(extension(HK2DebugFeature)) + + } + + private static ModuleItemInfoImpl module(Class ext, @DelegatesTo(ModuleItemInfoImpl) Closure config = null) { + return item(ConfigItem.Module, ext, config) + } + + private static InstallerItemInfoImpl installer(Class ext, @DelegatesTo(InstallerItemInfoImpl) Closure config = null) { + return item(ConfigItem.Installer, ext, config) + } + + private static GuiceyBundleItemInfoImpl bundle(Class ext, @DelegatesTo(GuiceyBundleItemInfoImpl) Closure config = null) { + return item(ConfigItem.Bundle, ext, config) + } + + private static DropwizardBundleItemInfoImpl dropwizardBundle(Class ext, @DelegatesTo(DropwizardBundleItemInfoImpl) Closure config = null) { + return item(ConfigItem.DropwizardBundle, ext, config) + } + + private static ExtensionItemInfoImpl extension(Class ext, @DelegatesTo(ExtensionItemInfoImpl) Closure config = null) { + return item(ConfigItem.Extension, ext, config) + } + + private static T item(ConfigItem itemType, Class type, Closure config = null) { + T res = itemType.newContainer(type) + if (config) { + res.with config + } + return res; + } +} \ No newline at end of file diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy index 86a9504d5..19b15ec22 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/InstanceItemsTest.groovy @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy index 2470e6080..26a7fc7e6 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ModuleOverrideTest.groovy @@ -2,16 +2,16 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.AbstractModule import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy index e414e8137..1aa9cbf15 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/NoInterfaceBindTest.groovy @@ -2,14 +2,14 @@ package ru.vyarus.dropwizard.guice.config import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.config.support.NoIfaceBindingApp import ru.vyarus.dropwizard.guice.config.support.conf.* import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Unroll -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy index 6db891ad8..e2c47c42a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/ScopeRecognitionTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.config -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup import ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/Component.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/Component.groovy new file mode 100644 index 000000000..b8a5636b1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/Component.groovy @@ -0,0 +1,10 @@ +package ru.vyarus.dropwizard.guice.config.configfilter + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@interface Component {} \ No newline at end of file diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/bnd/AutoScanBindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/bnd/AutoScanBindingsTest.groovy new file mode 100644 index 000000000..63484cf43 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/bnd/AutoScanBindingsTest.groovy @@ -0,0 +1,63 @@ +package ru.vyarus.dropwizard.guice.config.configfilter.bnd + +import com.google.inject.AbstractModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import jakarta.inject.Inject +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.config.configfilter.Component +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(App) +class AutoScanBindingsTest extends AbstractTest { + @Inject + GuiceyConfigurationInfo info + + def "Check filters applied"() { + + expect: + info.getExtensions().contains(Ext1) + !info.getExtensions().contains(Ext2) + // guice bindings would also appear in filter! + App.filtered.containsAll([Ext1, Ext2]) + } + + static class App extends Application { + static List> filtered = [] + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .autoConfigFilter { + filtered.add(it) + return it.isAnnotationPresent(Component) + } + .modules(new AbstractModule() { + @Override + protected void configure() { + bind(Ext1) + bind(Ext2) + } + }) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Component + static class Ext1 implements Managed {} + + static class Ext2 implements Managed {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/ext/AutoScanFilterTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/ext/AutoScanFilterTest.groovy new file mode 100644 index 000000000..f57c4ad53 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/configfilter/ext/AutoScanFilterTest.groovy @@ -0,0 +1,60 @@ +package ru.vyarus.dropwizard.guice.config.configfilter.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import jakarta.inject.Inject +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.GuiceyOptions +import ru.vyarus.dropwizard.guice.config.configfilter.Component +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(App) +class AutoScanFilterTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check filters applied"() { + + expect: + info.getExtensions().contains(Ext1) + !info.getExtensions().contains(Ext2) + // guice bindings would also appear in filter! + App.filtered.containsAll([Ext1, Ext2]) + } + + static class App extends Application { + static List> filtered = [] + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .option(GuiceyOptions.ScanProtectedClasses, true) + .autoConfigFilter { + filtered.add(it) + return it.isAnnotationPresent(Component) + } + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Component + static class Ext1 implements Managed {} + + static class Ext2 implements Managed {} +} + diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableDropwizardBundleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableDropwizardBundleTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableDropwizardBundleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableDropwizardBundleTest.groovy index 5fe74ad37..effabf327 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableDropwizardBundleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableDropwizardBundleTest.groovy @@ -1,11 +1,11 @@ -package ru.vyarus.dropwizard.guice.config +package ru.vyarus.dropwizard.guice.config.disable import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableFromBundlesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableFromBundlesTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableFromBundlesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableFromBundlesTest.groovy index d19882756..8f0927e32 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableFromBundlesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableFromBundlesTest.groovy @@ -1,10 +1,10 @@ -package ru.vyarus.dropwizard.guice.config +package ru.vyarus.dropwizard.guice.config.disable import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableItemsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableItemsTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableItemsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableItemsTest.groovy index d46c04d5e..94f132869 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableItemsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableItemsTest.groovy @@ -1,11 +1,11 @@ -package ru.vyarus.dropwizard.guice.config +package ru.vyarus.dropwizard.guice.config.disable import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -15,8 +15,8 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableWithPredicateTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableWithPredicateTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableWithPredicateTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableWithPredicateTest.groovy index 76cd43183..2a4dc661f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisableWithPredicateTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisableWithPredicateTest.groovy @@ -1,11 +1,11 @@ -package ru.vyarus.dropwizard.guice.config +package ru.vyarus.dropwizard.guice.config.disable import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook @@ -17,8 +17,8 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path import static ru.vyarus.dropwizard.guice.module.context.Disables.type import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicateByInstallerTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicateByInstallerTest.groovy new file mode 100644 index 000000000..c065ccb21 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicateByInstallerTest.groovy @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.config.disable + +import com.google.inject.Binder +import com.google.inject.Inject +import com.google.inject.Module +import jakarta.servlet.annotation.WebFilter +import jakarta.servlet.http.HttpFilter +import org.junit.jupiter.api.Test +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.GuiceyOptions +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.Disables +import ru.vyarus.dropwizard.guice.module.installer.scanner.InvisibleForScanner +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +import static ru.vyarus.dropwizard.guice.test.util.ClassFilters.declaredIn + +/** + * @author Vyacheslav Rusakov + * @since 21.02.2025 + */ +@TestGuiceyApp(App) +class DisablesPredicateByInstallerTest { + + @Inject + GuiceyConfigurationInfo info + + @Test + void testDisableByInstaller() { + expect: + info.getExtensionsDisabled() == [DirectFilter, FilterFromScan, FilterFromBinding] + } + + static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .enableAutoConfig() + // scan only inside class + .autoConfigFilter(declaredIn(DisablesPredicateByInstallerTest)) + .option(GuiceyOptions.ScanProtectedClasses, true) + .extensions(DirectFilter) + .modules(new Module() { + @Override + void configure(Binder binder) { + binder.bind(FilterFromBinding.class) + } + }) + .disable(Disables.webExtension()) + .build() + } + } + + @WebFilter + @InvisibleForScanner + static class DirectFilter extends HttpFilter { + + } + + @WebFilter + @InvisibleForScanner + static class FilterFromBinding extends HttpFilter { + + } + + @WebFilter + static class FilterFromScan extends HttpFilter { + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicatesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicatesTest.groovy new file mode 100644 index 000000000..15d525206 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/disable/DisablesPredicatesTest.groovy @@ -0,0 +1,173 @@ +package ru.vyarus.dropwizard.guice.config.disable + +import com.google.inject.Binder +import com.google.inject.Module +import io.dropwizard.core.Application +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.module.context.ConfigItem +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.module.context.Disables +import ru.vyarus.dropwizard.guice.module.context.info.DropwizardBundleItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.GuiceyBundleItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.InstallerItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.ItemId +import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.ModuleItemInfo +import ru.vyarus.dropwizard.guice.module.context.info.impl.DropwizardBundleItemInfoImpl +import ru.vyarus.dropwizard.guice.module.context.info.impl.ExtensionItemInfoImpl +import ru.vyarus.dropwizard.guice.module.context.info.impl.GuiceyBundleItemInfoImpl +import ru.vyarus.dropwizard.guice.module.context.info.impl.InstallerItemInfoImpl +import ru.vyarus.dropwizard.guice.module.context.info.impl.ItemInfoImpl +import ru.vyarus.dropwizard.guice.module.context.info.impl.ModuleItemInfoImpl +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.health.HealthCheckInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller +import ru.vyarus.dropwizard.guice.module.installer.feature.web.WebFilterInstaller + +import static ru.vyarus.dropwizard.guice.module.context.ConfigItem.* + +/** + * @author Vyacheslav Rusakov + * @since 09.04.2018 + */ +class DisablesPredicatesTest extends AbstractTest { + + def "Check predicates"() { + + expect: + Disables.registeredBy(ConfigScope.Application).test(item(Extension, Sample)) + Disables.registeredBy(ConfigScope.Application.getKey()).test(item(Extension, Sample)) + !Disables.registeredBy(Serializable).test(item(Extension, Sample)) + !Disables.registeredBy(ItemId.from(Serializable)).test(item(Extension, Sample)) + + Disables.itemType(Extension, Installer).test(item(Installer, Sample)) + !Disables.itemType(Extension, Installer).test(item(Bundle, Sample)) + + Disables.extension().test(item(Extension, Sample)) + !Disables.extension().test(item(Installer, Sample)) + + Disables.installer().test(item(Installer, Sample)) + !Disables.installer().test(item(Extension, Sample)) + + Disables.module().test(item(ConfigItem.Module, Sample)) + !Disables.module().test(item(Extension, Sample)) + + Disables.bundle().test(item(Bundle, Sample)) + !Disables.bundle().test(item(Extension, Sample)) + + Disables.dropwizardBundle().test(item(DropwizardBundle, Sample)) + !Disables.dropwizardBundle().test(item(Bundle, Sample)) + + Disables.type(Sample).test(item(Extension, Sample)) + !Disables.type(Sample).test(item(Extension, Sample2)) + + Disables.inPackage('ru.vyarus').test(item(Extension, Sample)) + !Disables.inPackage('com.foo').test(item(Extension, Sample2)) + } + + def "Check extension predicates"() { + + expect: + Disables.extension().and {it.installedBy == ManagedInstaller } + .test(extension(Sample, ManagedInstaller)) + !Disables.extension().and {it.installedBy == HealthCheckInstaller } + .test(extension(Sample, ManagedInstaller)) + + !Disables.installedBy(HealthCheckInstaller).test(extension(Sample, ManagedInstaller)) + Disables.installedBy(HealthCheckInstaller).test(extension(Sample, HealthCheckInstaller)) + + Disables.webExtension().test(extension(Sample, WebFilterInstaller)) + !Disables.webExtension().test(extension(Sample, ManagedInstaller)) + Disables.jerseyExtension().test(extension(Sample, ResourceInstaller)) + !Disables.jerseyExtension().test(extension(Sample, ManagedInstaller)) + + !Disables.module().and {it.overriding}.test(module()) + Disables.module().and {it.overriding}.test(module(true)) + + !Disables.bundle().and {it.fromLookup}.test(bundle(Sample)) + Disables.bundle().and {it.fromLookup}.test(bundle(Sample, true)) + + Disables.dropwizardBundle().and {it.enabled}.test(dropwizardBundle(Sample)) + !Disables.dropwizardBundle().and {it.enabled}.test(dropwizardBundle(Sample, true)) + + Disables.installer().and {it.enabled}.test(installer(Sample)) + !Disables.installer().and {it.enabled}.test(installer(Sample, true)) + } + + def "Check composition"() { + + def predicate = Disables.registeredBy(Application) + .and(Disables.installer()) + .and(Disables.type(Sample2).negate()) + + expect: + predicate.test(item(Installer, Sample)) + !predicate.test(item(Extension, Sample)) + !predicate.test(item(Installer, Sample2)) + !predicate.test(item(Installer, Sample, Sample)) + } + + ItemInfo item(ConfigItem type, Class cls, Class from = Application) { + ItemInfoImpl info = new ItemInfoImpl(type, ItemId.from(cls)) + info.countRegistrationAttempt(ItemId.from(from)) + return info; + } + + ExtensionItemInfo extension(Class type, Class installer) { + ExtensionItemInfoImpl res = new ExtensionItemInfoImpl(type) + res.setInstaller(installer.constructors[0].newInstance() as FeatureInstaller) + return res + } + + ModuleItemInfo module(boolean overriding = false) { + Closure create = { + new ModuleItemInfoImpl(new Module() { + @Override + void configure(Binder binder) { + } + }) + } + ModuleItemInfo res + + if (overriding) { + ModuleItemInfoImpl.overrideScope { + res = create() + } + } else { + res = create() + } + + return res + } + + GuiceyBundleItemInfo bundle(Class type, boolean fromLookup = false) { + GuiceyBundleItemInfoImpl res = new GuiceyBundleItemInfoImpl(type) + if (fromLookup) { + res.countRegistrationAttempt(ConfigScope.BundleLookup.key) + } + return res + } + + DropwizardBundleItemInfo dropwizardBundle(Class type, boolean disabled = false) { + DropwizardBundleItemInfoImpl res = new DropwizardBundleItemInfoImpl(type) + if (disabled) { + res.disabledBy.add(ConfigScope.GuiceyBundle.key) + } + return res + } + + InstallerItemInfo installer(Class type, boolean disabled = false) { + InstallerItemInfoImpl res = new InstallerItemInfoImpl(type) + if (disabled) { + res.disabledBy.add(ConfigScope.GuiceyBundle.key) + } + return res + } + + + static class Sample {} + + static class Sample2 {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy index 6d7ca3ab8..30f1762c5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/InstallerOptionsTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.option -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.installer.option.InstallerOptionsSuppor import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.installer.InstallersOptions.DenyServletRegistrationWithClash diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionHolderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionHolderTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionHolderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionHolderTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy index b1698a34c..1417c28db 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsAccessTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.option -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions import ru.vyarus.dropwizard.guice.config.option.support.OtherOptions @@ -18,7 +18,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsSupportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsSupportTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsSupportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/OptionsSupportTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionParserTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionParserTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionParserTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionParserTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionsMappingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionsMappingTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionsMappingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptionsMappingTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy index bf75538fd..aea08d96d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/OptsMappingTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.option.mapper import com.google.inject.Stage -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.junit.jupiter.api.extension.ExtendWith import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/StringConverterTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/StringConverterTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/StringConverterTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/mapper/StringConverterTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/OtherOptions.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/OtherOptions.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/OtherOptions.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/OtherOptions.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/SampleOptions.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/SampleOptions.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/SampleOptions.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/option/support/SampleOptions.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy index ab6e4ad4d..43b5dadd3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.optionalext -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -14,8 +14,8 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstal import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy similarity index 99% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy index c7126e7dd..921123829 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalExtensionsReportTest.groovy @@ -8,7 +8,7 @@ import ru.vyarus.dropwizard.guice.debug.report.tree.ContextTreeRenderer import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalRuntimeExtensionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalRuntimeExtensionTest.groovy new file mode 100644 index 000000000..41f861445 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/optionalext/OptionalRuntimeExtensionTest.groovy @@ -0,0 +1,77 @@ +package ru.vyarus.dropwizard.guice.config.optionalext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import jakarta.ws.rs.Path +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller +import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ +@TestGuiceyApp(App) +class OptionalRuntimeExtensionTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check optional extensions support"() { + + expect: "one extension accepted" + info.getExtensions() == [ExtAccepted, HK2DebugFeature, ExtAccepted2] + + and: "other disabled automatically" + info.getExtensionsDisabled() == [ExtDisabled, ExtDisabled2] + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .noDefaultInstallers() + .installers(ResourceInstaller) + .extensionsOptional(ExtAccepted, ExtDisabled) + .bundles(new Bundle()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } + + static class Bundle implements GuiceyBundle { + + @Override + void run(GuiceyEnvironment environment) throws Exception { + environment.extensionsOptional( + ExtAccepted2, ExtDisabled2 + ) + } + } + + @Path("/1") + static class ExtAccepted {} + + @Path("/2") + static class ExtAccepted2 {} + + @EagerSingleton + static class ExtDisabled {} + + @EagerSingleton + static class ExtDisabled2 {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/NoProtectedClassesScanTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/NoProtectedClassesScanTest.groovy new file mode 100644 index 000000000..f3ef4c466 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/NoProtectedClassesScanTest.groovy @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.config.protect + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 28.01.2025 + */ +@TestGuiceyApp(App) +class NoProtectedClassesScanTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check default behavior"() { + + expect: "only public detected" + info.getExtensions().contains(PublicExt1) + !info.getExtensions().contains(ProtectedExt1) + !info.getExtensions().contains(PublicExt1.ProtectedExt2) + !info.getExtensions().contains(ProtectedExt3) + !info.getExtensions().contains(PublicExt1.ProtectedExt4) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedClassesScanTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedClassesScanTest.groovy new file mode 100644 index 000000000..1ef490a0a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedClassesScanTest.groovy @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.config.protect + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.GuiceyOptions +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 28.01.2025 + */ +@TestGuiceyApp(App) +class ProtectedClassesScanTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check protected classes scan"() { + + expect: "only public detected" + info.getExtensions().contains(PublicExt1) + info.getExtensions().contains(ProtectedExt1) + info.getExtensions().contains(PublicExt1.ProtectedExt2) + !info.getExtensions().contains(ProtectedExt3) + !info.getExtensions().contains(PublicExt1.ProtectedExt4) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .option(GuiceyOptions.ScanProtectedClasses, true) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt1.java new file mode 100644 index 000000000..ae9e02762 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt1.java @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.config.protect; + +import io.dropwizard.lifecycle.Managed; + +/** + * @author Vyacheslav Rusakov + * @since 28.01.2025 + */ +class ProtectedExt1 implements Managed { + + @Override + public void start() throws Exception { + Managed.super.start(); + } + + @Override + public void stop() throws Exception { + Managed.super.stop(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt3.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt3.java new file mode 100644 index 000000000..7e837eeb9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/ProtectedExt3.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.config.protect; + +import io.dropwizard.lifecycle.Managed; +import ru.vyarus.dropwizard.guice.module.installer.scanner.InvisibleForScanner; + +/** + * @author Vyacheslav Rusakov + * @since 28.01.2025 + */ +@InvisibleForScanner +public class ProtectedExt3 implements Managed { + + @Override + public void start() throws Exception { + Managed.super.start(); + } + + @Override + public void stop() throws Exception { + Managed.super.stop(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/PublicExt1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/PublicExt1.java new file mode 100644 index 000000000..cc2eefae1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/protect/PublicExt1.java @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.config.protect; + +import io.dropwizard.lifecycle.Managed; +import ru.vyarus.dropwizard.guice.module.installer.scanner.InvisibleForScanner; + +/** + * @author Vyacheslav Rusakov + * @since 28.01.2025 + */ +public class PublicExt1 implements Managed { + + @Override + public void start() throws Exception { + Managed.super.start(); + } + + @Override + public void stop() throws Exception { + Managed.super.stop(); + } + + protected static class ProtectedExt2 implements Managed { + @Override + public void stop() throws Exception { + Managed.super.stop(); + } + + @Override + public void start() throws Exception { + Managed.super.start(); + } + } + + @InvisibleForScanner + protected static class ProtectedExt4 implements Managed { + @Override + public void stop() throws Exception { + Managed.super.stop(); + } + + @Override + public void start() throws Exception { + Managed.super.start(); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy similarity index 50% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy index 70c5c48ed..c023ad26a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedHookStateTest.groovy @@ -1,19 +1,19 @@ package ru.vyarus.dropwizard.guice.config.sharedstate -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject - /** * @author Vyacheslav Rusakov * @since 28.09.2019 @@ -24,10 +24,14 @@ class SharedHookStateTest extends Specification { @Inject Bootstrap bootstrap - def "Check hook access to shared memeory"() { + def "Check hook access to shared memory"() { expect: - SharedConfigurationState.lookup(bootstrap.getApplication(), XHook).get() == "12" + SharedConfigurationState.lookup(bootstrap.getApplication(), XHookState).get().value == "12" + SharedConfigurationState.lookup(bootstrap.getApplication(), BundleState).get().value == "15" + SharedConfigurationState.lookup(bootstrap.getApplication(), SharedHookState).get().value == "20" + Bundle.called + Bundle.called2 } static class App extends Application { @@ -47,15 +51,39 @@ class SharedHookStateTest extends Specification { @Override void configure(GuiceBundle.Builder builder) { builder.withSharedState({ - it.put(XHook, "12") + it.put(XHookState, new XHookState(value: "12")) }) } } static class Bundle implements GuiceyBundle { + + static boolean called + static boolean called2 + @Override void initialize(GuiceyBootstrap bootstrap) { - assert bootstrap.sharedStateOrFail(XHook, "ugr") == "12" + bootstrap.shareState(SharedHookState, new SharedHookState(value: "20")) + bootstrap.whenSharedStateReady(BundleState, { assert it.value == "15"; called = true }) + assert bootstrap.sharedStateOrFail(XHookState, "problem").value == "12" + } + + @Override + void run(GuiceyEnvironment environment) throws Exception { + environment.whenSharedStateReady(BundleState, { assert it.value == "15"; called2 = true}) + environment.shareState(BundleState, new BundleState(value: "15")) } } + + static class XHookState { + String value + } + + static class BundleState { + String value + } + + static class SharedHookState { + String value + } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy similarity index 62% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy index 8aba19423..40031d60a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateAccessInModuleTest.groovy @@ -1,16 +1,16 @@ package ru.vyarus.dropwizard.guice.config.sharedstate -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -25,7 +25,7 @@ class SharedStateAccessInModuleTest extends AbstractTest { def "Check shared state access within module"() { expect: "module modified state" - SharedConfigurationState.lookup(environment, Mod).get()["module"] == "i was here" + SharedConfigurationState.lookup(environment, ModState).get()["module"] == "i was here" } static class App extends Application { @@ -33,7 +33,7 @@ class SharedStateAccessInModuleTest extends AbstractTest { void initialize(Bootstrap bootstrap) { bootstrap.addBundle(GuiceBundle.builder() // init sample value in shared state - .withSharedState({ it.put(Mod, new HashMap()) }) + .withSharedState({ it.put(ModState, new ModState()) }) .modules(new Mod()) .build()) } @@ -48,19 +48,22 @@ class SharedStateAccessInModuleTest extends AbstractTest { @Override protected void configure() { assert !sharedState(List).isPresent() - assert sharedState(Mod).isPresent() - def val = sharedStateOrFail(Mod, "No mod initialized") + assert sharedState(ModState).isPresent() + def val = sharedStateOrFail(ModState, "No mod initialized") assert val instanceof Map val.put("module", "i was here") - shareState(List, "fafa") - assert sharedState(List).get() == "fafa" + shareState(List, Collections.singletonList("fafa")) + assert sharedState(List).get()[0] == "fafa" - sharedState(List, { "ff" }) == "ff" - sharedState(Queue, { "tt" }) == "tt" + sharedState(List, { ["ff"] }).iterator().next() == "ff" + sharedState(Queue, { ["tt"] }).iterator().next() == "tt" } } + + static class ModState extends HashMap { + } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy index b2713ea0d..db8d8b84a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateDirectAccessTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.sharedstate -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState @@ -27,7 +27,7 @@ class SharedStateDirectAccessTest extends Specification { def "Check shared state direct availability"() { expect: - noState() + noStartupState() } static class App extends Application { @@ -40,7 +40,7 @@ class SharedStateDirectAccessTest extends Specification { @Override void run(Configuration configuration, Environment environment) throws Exception { - noState() + noStartupState() } } @@ -50,6 +50,7 @@ class SharedStateDirectAccessTest extends Specification { protected void configurationHooksProcessed(ConfigurationHooksProcessedEvent event) { def state = SharedConfigurationState.getStartupInstance() assert state + assert state.options noBootstrap(state) } @@ -57,6 +58,7 @@ class SharedStateDirectAccessTest extends Specification { protected void dropwizardBundlesInitialized(DropwizardBundlesInitializedEvent event) { def state = SharedConfigurationState.getStartupInstance() assert state + assert state.options assert state.bootstrap.get() noEnvironment(state) } @@ -65,6 +67,7 @@ class SharedStateDirectAccessTest extends Specification { protected void initialized(InitializedEvent event) { def state = SharedConfigurationState.getStartupInstance() assert state + assert state.options assert state.bootstrap.get() noEnvironment(state) } @@ -73,6 +76,7 @@ class SharedStateDirectAccessTest extends Specification { protected void beforeRun(BeforeRunEvent event) { def state = SharedConfigurationState.getStartupInstance() assert state + assert state.options assert state.bootstrap.get() assert state.environment.get() assert state.configuration.get() @@ -84,14 +88,15 @@ class SharedStateDirectAccessTest extends Specification { protected void applicationRun(ApplicationRunEvent event) { def state = SharedConfigurationState.getStartupInstance() assert state + assert state.options assert state.bootstrap.get() assert state.environment.get() - noInjector(state) + assert state.injector.get() } @Override protected void applicationStarted(ApplicationStartedEvent event) { - noState() + noStartupState() } } @@ -131,7 +136,7 @@ class SharedStateDirectAccessTest extends Specification { } } - private static boolean noState() { + private static boolean noStartupState() { try { SharedConfigurationState.getStartupInstance() assert false diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy similarity index 74% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy index fea15634f..4e60a1ec8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateTest.groovy @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice.config.sharedstate -import io.dropwizard.Application -import io.dropwizard.Configuration +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration import io.dropwizard.jetty.MutableServletContextHandler import io.dropwizard.lifecycle.setup.LifecycleEnvironment -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState import spock.lang.Specification @@ -74,10 +74,14 @@ class SharedStateTest extends Specification { SharedConfigurationState.lookup(app, App).get() == app SharedConfigurationState.lookupOrFail(app, App, "2") == app + and: "access or create work" + SharedConfigurationState.lookupOrCreate(app, CustomState, () -> new CustomState()) + SharedConfigurationState.lookupOrFail(app, CustomState, "err") + when: "to string state" res = state.toString() then: "ok" - res == "Shared state with 1 objects: $App.name" + res == "Shared state with 2 objects: $CustomState.name, $App.name" } @@ -101,7 +105,8 @@ class SharedStateTest extends Specification { when: "get with null supplier" def res = state.get(App, null) then: "behave as usual get" - res == null + def exN = thrown(NullPointerException) + exN.message == "Cannot invoke \"java.util.function.Supplier.get()\" because \"defaultValue\" is null" when: "duplicate assign" state.assignTo app @@ -158,10 +163,43 @@ class SharedStateTest extends Specification { SharedConfigurationState.lookup(environment, App).get() == app SharedConfigurationState.lookupOrFail(environment, App, "2") == app + and: "access and create" + SharedConfigurationState.lookupOrCreate(environment, CustomState, () -> new CustomState()) + SharedConfigurationState.lookupOrFail(environment, CustomState, "err") + when: "to string state" res = state.toString() then: "ok" - res == "Shared state with 1 objects: $App.name" + res == "Shared state with 2 objects: $CustomState.name, $App.name" + } + + def "Check auto closable support on shutdown"() { + + setup: "prepare state" + SharedConfigurationState state = new SharedConfigurationState() + + when: "register closable object" + CustomState val = new CustomState() + state.put(CustomState, val) + state.shutdown() + + then: "cleanup called" + val.called + } + + def "Check auto closable support on clean"() { + + setup: "prepare state" + SharedConfigurationState state = new SharedConfigurationState() + state.assignTo(new App()) + + when: "register closable object" + CustomState val = new CustomState() + state.put(CustomState, val) + SharedConfigurationState.clear() + + then: "cleanup called" + val.called } static class App extends Application { @@ -174,4 +212,14 @@ class SharedStateTest extends Specification { void run(Configuration configuration, Environment environment) throws Exception { } } + + static class CustomState implements AutoCloseable { + + boolean called + + @Override + void close() throws Exception { + called = true + } + } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy similarity index 54% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy index a6f0b3788..4763cb392 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/sharedstate/SharedStateUsageTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.sharedstate -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap @@ -22,7 +22,8 @@ class SharedStateUsageTest extends Specification { def "Check shared state usage"() { expect: "all asserts ok" - true + GlobalBundle.called + EqualBundle.called } static class App extends Application { @@ -36,9 +37,9 @@ class SharedStateUsageTest extends Specification { def app = bootstrap.getApplication() assert SharedConfigurationState.get(app) != null - assert SharedConfigurationState.lookup(app, GlobalBundle).isPresent() - assert !SharedConfigurationState.lookup(app, ChildBundle).isPresent() - assert SharedConfigurationState.lookup(app, EqualBundle).isPresent() + assert SharedConfigurationState.lookup(app, GlobalState).isPresent() + assert !SharedConfigurationState.lookup(app, ChildState).isPresent() + assert SharedConfigurationState.lookup(app, EqualState).isPresent() } @Override @@ -47,21 +48,23 @@ class SharedStateUsageTest extends Specification { } static class GlobalBundle implements GuiceyBundle { + static boolean called @Override void initialize(GuiceyBootstrap bootstrap) { - bootstrap.shareState(GlobalBundle, "12") - assert bootstrap.sharedState(GlobalBundle, null) != null + bootstrap.shareState(GlobalState, new GlobalState(value: "12")) + bootstrap.whenSharedStateReady(GlobalState, { assert it.value == "12"; called = true}) + assert bootstrap.sharedState(GlobalState, null) != null } } static class ChildBundle implements GuiceyBundle { @Override void initialize(GuiceyBootstrap bootstrap) { - assert bootstrap.sharedStateOrFail(GlobalBundle, "no state") == "12" + assert bootstrap.sharedStateOrFail(GlobalState, "no state").value == "12" try { // access other state - bootstrap.sharedStateOrFail(ChildBundle, "ups") + bootstrap.sharedStateOrFail(ChildState, "ups") assert false } catch (IllegalStateException ex) { assert ex.message == 'ups' @@ -70,37 +73,53 @@ class SharedStateUsageTest extends Specification { @Override void run(GuiceyEnvironment environment) throws Exception { - assert environment.sharedState(GlobalBundle).get() == "12" - assert environment.sharedStateOrFail(GlobalBundle, "sds") == "12" + assert environment.sharedState(GlobalState).get().value == "12" + assert environment.sharedStateOrFail(GlobalState, "sds").value == "12" try { // access other state - environment.sharedStateOrFail(ChildBundle, "ups") + environment.sharedStateOrFail(ChildState, "ups") assert false } catch (IllegalStateException ex) { assert ex.message == 'ups' } // check state sharing in run phase - environment.shareState(Map, "foo") - assert environment.sharedState(Map, null) == "foo" - assert environment.sharedState(List, { "baa" }) == "baa" + environment.shareState(String, "foo") + assert environment.sharedState(String, null) == "foo" + assert environment.sharedState(List, { ["baa"] })[0] == "baa" } } static class EqualBundle implements GuiceyBundle { + + static boolean called + @Override void initialize(GuiceyBootstrap bootstrap) { - def state = bootstrap.sharedState(EqualBundle, { "13" }) - assert state == "13" + def state = bootstrap.sharedState(EqualState, { new EqualState(value: "13") }) + assert state.value == "13" - assert bootstrap.sharedState(EqualBundle).get() == "13" + assert bootstrap.sharedState(EqualState).get().value == "13" } @Override void run(GuiceyEnvironment environment) throws Exception { - assert environment.sharedState(EqualBundle).get() == "13" - assert environment.sharedStateOrFail(EqualBundle, "sds") == "13" + environment.whenSharedStateReady(EqualState, { assert it.value == "13"; called = true}) + assert environment.sharedState(EqualState).get().value == "13" + assert environment.sharedStateOrFail(EqualState, "sds").value == "13" } } + + static class GlobalState { + String value + } + + static class ChildState { + String value + } + + static class EqualState { + String value + } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy similarity index 74% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy index 7bc9cc8b3..adbc5cb7e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/BasicApplication.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy index 766bbfecd..649599af0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/ComplexConfigApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.config.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.config.support.conf.ConfigLevel2 diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy index e72af9dcc..5b73fbec0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/NoIfaceBindingApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.config.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.config.support.conf.ConfigLevel2 diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy index f3d429536..40b244f5f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel1.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.config.support.conf -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/ConfigLevel2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1IndirectInterface.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1IndirectInterface.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1IndirectInterface.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1IndirectInterface.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1Interface.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1Interface.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1Interface.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level1Interface.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level2Interface.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level2Interface.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level2Interface.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/support/conf/Level2Interface.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy index 88be31ec9..c04b553d9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DeduplicatorOverrideTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.context.unique.LegacyModeDuplicatesDete import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy similarity index 82% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy index 0e2ccaaff..a6848dba0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DifferentClassLoaderRecognitionTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.config.unique.support.ParentLastURLClassLoader @@ -12,6 +12,7 @@ import ru.vyarus.dropwizard.guice.config.unique.support.SampleExt import ru.vyarus.dropwizard.guice.config.unique.support.SampleModule import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils import spock.lang.Specification /** @@ -50,7 +51,8 @@ class DifferentClassLoaderRecognitionTest extends Specification { bootstrap.addBundle(GuiceBundle.builder() .extensions(ext1, ext2) - .modules(new SampleModule(), cl.loadClass(SampleModule.name).newInstance()) + .modules(new SampleModule(), + InstanceUtils.create((Class) cl.loadClass(SampleModule.name))) .printDiagnosticInfo() .build() ); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy index b894462da..2964602d3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateBundlesReportingTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy index cda10b4f5..ef98e6d2d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateDropwizardBundlesReportingTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy index b9dff3154..d2627685d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateInstanceInitTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy index 58690d8e5..b19f48d6d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateModulesReportingTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy index 3fc79e5c4..c87cc9e72 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/DuplicateScopesReportingTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -17,8 +17,8 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy index 9f8f65b8a..85b098de9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/LegacyDuplicatesPolicyTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.Foo2Bundle diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy index 6667cb346..f500f5816 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultiSourceIgnoresTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy index cddf7a4de..140fcece3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleBundleInstancesTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.Foo2Bundle diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy index ce901c7ce..deee9cccf 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleDropwizardBundlesTest.groovy @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy index 9354a0a3f..6a167352e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/MultipleModuleInstancesTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy index 49e9bd5ce..3b507e8a5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/NoDropwizardBundlesTrackingTest.groovy @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy index 0840b6e96..cb3a0d17c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/Simple2DeduplicationReportTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy index ebacd1244..546d6ca26 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/SimpleDeduplicationReportTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy index 1253cae50..7f1cad227 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TransitiveDropwizardBundlesTest.groovy @@ -1,11 +1,11 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy index cf7fc3374..d3007ead2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/TreeReportInstancesOrderTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.tree.ContextTreeConfig diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy index 50ebabd47..77fdcd71a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueAwareModulesTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.config.unique -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueDuplicatesDetectorTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueDuplicatesDetectorTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueDuplicatesDetectorTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueDuplicatesDetectorTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy index 5138c1ed0..c7a7aae92 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsDeduplicatorTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.config.unique import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsEqualsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsEqualsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsEqualsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/UniqueItemsEqualsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/ParentLastURLClassLoader.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/ParentLastURLClassLoader.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/ParentLastURLClassLoader.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/ParentLastURLClassLoader.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleExt.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleExt.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleExt.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleExt.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/config/unique/support/SampleModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy index 3224b9f27..9817f5d7d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/AvailableInstallersPrintTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy index 925e7d53f..4aac421b8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticBundleTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticListenersEqualsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticListenersEqualsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticListenersEqualsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/DiagnosticListenersEqualsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy index b2442ca5e..f80421aae 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/EmptyConfigRendererTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.VoidBundleLookup import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpTest.groovy new file mode 100644 index 000000000..ec99a8455 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/ExtensionsHelpTest.groovy @@ -0,0 +1,38 @@ +package ru.vyarus.dropwizard.guice.debug + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 09.12.2022 + */ +@TestGuiceyApp(App) +class ExtensionsHelpTest extends Specification { + + def "Check extensions help"() { + // actual reporting checked manually (test used for reporting configuration) + + expect: "checks that reporting doesn't fail" + true + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printExtensionsHelp() + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy index 641466cdb..0141fe909 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceAopReportTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.AopModule import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy index 5e9c6ff95..886f91cfd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/GuiceBindingsReportTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy index 547b43420..2741bcd0e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/JerseyReportTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnosticTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnosticTest.java new file mode 100644 index 000000000..1509b5509 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/SharedStateDiagnosticTest.java @@ -0,0 +1,215 @@ +package ru.vyarus.dropwizard.guice.debug; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.Stack; + +/** + * @author Vyacheslav Rusakov + * @since 21.03.2025 + */ +public class SharedStateDiagnosticTest extends AbstractPlatformTest { + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + @Test + void test() { + + } + } + + public static class App extends DefaultTestApp { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new Bundle()) + .modules(new Mod()) + .printSharedStateUsage() + .withSharedState(state -> { + state.get(List.class); // miss + state.whenReady(List.class, list -> { + // delayed access + }); + state.get(List.class, () -> Arrays.asList("foo", "bar")); // set and get + state.getOrFail(List.class, "err"); + state.whenReady(List.class, list -> { + // immediate access + }); + state.get(GuiceBundle.class); // never set + state.whenReady(GuiceBundle.class, guiceBundle -> {}); // never set + + state.whenReady(ConfiguredBundle.class, guiceBundle -> {}); // never set (listener only) + }) + .build()); + + // direct access + SharedConfigurationState.lookupOrFail(this, List.class, "err"); + SharedConfigurationState.getStartupInstance().whenReady(List.class, list -> { + // immediate access + }); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + // direct access + SharedConfigurationState.lookupOrFail(environment, List.class, "err"); + // getStartupInstance() not available here (after guice bundle run) + SharedConfigurationState.get(environment).get().whenReady(List.class, list -> { + // immediate access + }); + } + } + + public static class Bundle implements GuiceyBundle { + @Override + public void initialize(GuiceyBootstrap bootstrap) { + bootstrap.sharedState(Map.class); // miss + bootstrap.whenSharedStateReady(Map.class, state -> { + // delayed + }); + bootstrap.sharedState(Map.class, HashMap::new); // miss + bootstrap.whenSharedStateReady(Map.class, state -> { + // immediate access + }); + bootstrap.sharedStateOrFail(Map.class, "err"); + bootstrap.shareState(Queue.class, new ArrayDeque()); // set + get + + bootstrap.sharedState(GuiceBundle.class); // never set + bootstrap.whenSharedStateReady(GuiceBundle.class, guiceBundle -> {}); // never set + } + + @Override + public void run(GuiceyEnvironment environment) throws Exception { + environment.sharedState(Set.class); // miss + environment.whenSharedStateReady(Set.class, state -> { + // delayed + }); + environment.sharedState(Set.class, HashSet::new); // miss + environment.whenSharedStateReady(Set.class, state -> { + // immediate access + }); + environment.sharedStateOrFail(Set.class, "err"); + environment.shareState(Stack.class, new Stack()); // set + get + + environment.sharedState(GuiceBundle.class); // never set + environment.whenSharedStateReady(GuiceBundle.class, guiceBundle -> {}); // never set + } + } + + public static class Mod extends DropwizardAwareModule { + @Override + protected void configure() { + sharedState(Module.class); // miss + whenSharedStateReady(Module.class, state -> { + // delayed + }); + sharedState(Module.class, () -> new AbstractModule() {}); // miss + whenSharedStateReady(Module.class, state -> { + // immediate access + }); + sharedStateOrFail(Module.class, "err"); + shareState(AbstractModule.class, new AbstractModule() {}); // set + get + + sharedState(GuiceBundle.class); // never set + whenSharedStateReady(GuiceBundle.class, guiceBundle -> {}); // never set + } + } + + @Test + void testStateAccessTracking() { + String out = runSuccess(Test1.class); + + Assertions.assertThat(out).contains("Shared configuration state usage: \n" + + "\n" + + "\tSET Options (ru.vyarus.dropwizard.guice.module.context.option) \t at r.v.d.g.m.context.(ConfigurationContext.java:169)\n" + + "\n" + + "\tSET List (java.util) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:56)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:57)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:61)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:62)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:73)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:74)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:82)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:84)\n" + + "\n" + + "\tSET Bootstrap (io.dropwizard.core.setup) \t at r.v.d.g.m.context.(ConfigurationContext.java:808)\n" + + "\n" + + "\tSET Map (java.util) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:93)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:94)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:98)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:101)\n" + + "\n" + + "\tSET Queue (java.util) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:102)\n" + + "\tSET Configuration (io.dropwizard.core) \t at r.v.d.g.m.context.(ConfigurationContext.java:835)\n" + + "\tSET ConfigurationTree (ru.vyarus.dropwizard.guice.module.yaml) \t at r.v.d.g.m.context.(ConfigurationContext.java:836)\n" + + "\tSET Environment (io.dropwizard.core.setup) \t at r.v.d.g.m.context.(ConfigurationContext.java:837)\n" + + "\n" + + "\tSET Set (java.util) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:114)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:110)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:111)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:114)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:115)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:118)\n" + + "\n" + + "\tSET Stack (java.util) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:119)\n" + + "\n" + + "\tSET Module (com.google.inject) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:133)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:129)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:130)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:133)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:134)\n" + + "\t\tGET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:137)\n" + + "\n" + + "\tSET AbstractModule (com.google.inject) \t at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:138)\n" + + "\tSET Injector (com.google.inject) \t at r.v.d.g.i.lookup.(InjectorLookup.java:72)\n" + + "\n" + + "\tNEVER SET GuiceBundle (ru.vyarus.dropwizard.guice)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:65)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:104)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:121)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:140)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:66)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:105)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:122)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:141)\n" + + "\n" + + "\tNEVER SET ConfiguredBundle (io.dropwizard.core)\n" + + "\t\tMISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:68)"); + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/StartupDiagnosticTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/StartupDiagnosticTest.java new file mode 100644 index 000000000..6afaafbaa --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/StartupDiagnosticTest.java @@ -0,0 +1,514 @@ +package ru.vyarus.dropwizard.guice.debug; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.lifecycle.Managed; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.debug.report.start.DropwizardBundlesTracker; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; + +/** + * @author Vyacheslav Rusakov + * @since 11.03.2025 + */ +public class StartupDiagnosticTest extends AbstractPlatformTest { + + @Test + void testGuiceyRunReport() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("Application startup time: \n" + + "\n" + + "\tJVM time before : 111 ms \n" + + "\n" + + "\tApplication startup : 111 ms \n" + + "\t\tDropwizard initialization : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms (finished since start at 111 ms )\n" + + "\t\t\t\tBundle builder time : 111 ms \n" + + "\t\t\t\tHooks processing : 111 ms \n" + + "\t\t\t\t\tStartupDiagnosticTest$Test1$$Lambda$111/1111111: 111 ms \n" + + "\t\t\t\tClasspath scan : 111 ms \n" + + "\t\t\t\tCommands processing : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\tBundles lookup : 111 ms \n" + + "\t\t\t\tGuicey bundles init : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tInstallers resolution : 111 ms \n" + + "\t\t\t\t\tScanned extensions recognition : 111 ms \n" + + "\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\tConfigurationHooksProcessedEvent : 111 ms \n" + + "\t\t\t\t\tBeforeInitEvent : 111 ms \n" + + "\t\t\t\t\tBundlesResolvedEvent : 111 ms \n" + + "\t\t\t\t\tBundlesInitializedEvent : 111 ms \n" + + "\t\t\t\t\tCommandsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInstallersResolvedEvent : 111 ms \n" + + "\t\t\t\t\tClasspathExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInitializedEvent : 111 ms \n" + + "\n" + + "\t\tDropwizard run : 111 ms \n" + + "\t\t\tConfiguration and Environment : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms \n" + + "\t\t\t\tConfiguration analysis : 111 ms \n" + + "\t\t\t\tGuicey bundles run : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tGuice modules processing : 111 ms \n" + + "\t\t\t\t\tBindings resolution : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tExtensions registration : 111 ms \n" + + "\t\t\t\t\tGuice bindings analysis : 111 ms \n" + + "\t\t\t\t\tExtensions installation : 111 ms \n" + + "\t\t\t\tInjector creation : 111 ms"); + + Assertions.assertThat(out).contains("Listeners time : 111 ms \n" + + "\t\t\t\t\tBeforeRunEvent : 111 ms \n" + + "\t\t\t\t\tBundlesStartedEvent : 111 ms \n" + + "\t\t\t\t\tModulesAnalyzedEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInjectorCreationEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledByEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledEvent : 111 ms \n" + + "\t\t\t\t\tApplicationRunEvent : 111 ms \n" + + "\n" + + "\t\tWeb server startup : 111 ms \n" + + "\t\t\tLifecycle simulation time : 111 ms \n" + + "\t\t\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\t\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\t\t\tmanaged DummyManaged : 111 ms \n" + + "\t\t\t\tGuicey time : 111 ms \n" + + "\t\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\t\tApplicationStartingEvent : 111 ms \n" + + "\t\t\t\t\t\tApplicationStartedEvent : 111 ms"); + + Assertions.assertThat(out).contains("Application shutdown time: \n" + + "\n" + + "\tApplication shutdown : 111 ms \n" + + "\t\tmanaged DummyManaged : 111 ms \n" + + "\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\tListeners time : 111 ms \n" + + "\t\t\tApplicationShutdownEvent : 111 ms \n" + + "\t\t\tApplicationStoppedEvent : 111 ms"); + } + + @Test + void testDwRunReport() { + String out = run(Test2.class); + Assertions.assertThat(out).contains("Application startup time: \n" + + "\n" + + "\tJVM time before : 111 ms \n" + + "\n" + + "\tApplication startup : 111 ms \n" + + "\t\tDropwizard initialization : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms (finished since start at 111 ms )\n" + + "\t\t\t\tBundle builder time : 111 ms \n" + + "\t\t\t\tHooks processing : 111 ms \n" + + "\t\t\t\t\tStartupDiagnosticTest$Test2$$Lambda$111/1111111: 111 ms \n" + + "\t\t\t\tClasspath scan : 111 ms \n" + + "\t\t\t\tCommands processing : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\tBundles lookup : 111 ms \n" + + "\t\t\t\tGuicey bundles init : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tInstallers resolution : 111 ms \n" + + "\t\t\t\t\tScanned extensions recognition : 111 ms \n" + + "\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\tConfigurationHooksProcessedEvent : 111 ms \n" + + "\t\t\t\t\tBeforeInitEvent : 111 ms \n" + + "\t\t\t\t\tBundlesResolvedEvent : 111 ms \n" + + "\t\t\t\t\tBundlesInitializedEvent : 111 ms \n" + + "\t\t\t\t\tCommandsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInstallersResolvedEvent : 111 ms \n" + + "\t\t\t\t\tClasspathExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInitializedEvent : 111 ms \n" + + "\n" + + "\t\tDropwizard run : 111 ms \n" + + "\t\t\tConfiguration and Environment : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms \n" + + "\t\t\t\tConfiguration analysis : 111 ms \n" + + "\t\t\t\tGuicey bundles run : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tGuice modules processing : 111 ms \n" + + "\t\t\t\t\tBindings resolution : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tExtensions registration : 111 ms \n" + + "\t\t\t\t\tGuice bindings analysis : 111 ms \n" + + "\t\t\t\t\tExtensions installation : 111 ms \n" + + "\t\t\t\tInjector creation : 111 ms "); + + Assertions.assertThat(out).contains("Listeners time : 111 ms \n" + + "\t\t\t\t\tBeforeRunEvent : 111 ms \n" + + "\t\t\t\t\tBundlesStartedEvent : 111 ms \n" + + "\t\t\t\t\tModulesAnalyzedEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInjectorCreationEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledByEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledEvent : 111 ms \n" + + "\t\t\t\t\tApplicationRunEvent : 111 ms \n" + + "\n" + + "\t\tWeb server startup : 111 ms \n" + + "\t\t\tJetty lifecycle time : 111 ms \n" + + "\t\t\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\t\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\t\t\tmanaged DummyManaged : 111 ms \n" + + "\t\t\t\tJersey time : 111 ms \n" + + "\t\t\t\t\tGuicey time : 111 ms \n" + + "\t\t\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\t\t\tApplicationStartingEvent : 111 ms \n" + + "\t\t\t\t\t\t\tJerseyConfigurationEvent : 111 ms \n" + + "\t\t\t\t\t\t\tJerseyExtensionsInstalledByEvent : 111 ms \n" + + "\t\t\t\t\t\t\tJerseyExtensionsInstalledEvent : 111 ms \n" + + "\t\t\t\t\t\t\tApplicationStartedEvent : 111 ms"); + + Assertions.assertThat(out).contains("Application shutdown time: \n" + + "\n" + + "\tApplication shutdown : 111 ms \n" + + "\t\tmanaged DummyManaged : 111 ms \n" + + "\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\tListeners time : 111 ms \n" + + "\t\t\tApplicationShutdownEvent : 111 ms \n" + + "\t\t\tApplicationStoppedEvent : 111 ms"); + } + + @Test + void testRestStubsRunReport() { + String out = run(Test3.class); + Assertions.assertThat(out).contains("Application startup time: \n" + + "\n" + + "\tJVM time before : 111 ms \n" + + "\n" + + "\tApplication startup : 111 ms \n" + + "\t\tDropwizard initialization : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms (finished since start at 111 ms )\n" + + "\t\t\t\tBundle builder time : 111 ms \n" + + "\t\t\t\tHooks processing : 111 ms \n" + + "\t\t\t\t\tRestStubsHook : 111 ms \n" + + "\t\t\t\t\tRestStubFieldsSupport : 111 ms \n" + + "\t\t\t\t\tStartupDiagnosticTest$Test3$$Lambda$111/1111111: 111 ms \n" + + "\t\t\t\tClasspath scan : 111 ms \n" + + "\t\t\t\tCommands processing : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\t\tNonInjactableCommand : 111 ms \n" + + "\t\t\t\tBundles lookup : 111 ms \n" + + "\t\t\t\tGuicey bundles init : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tInstallers resolution : 111 ms \n" + + "\t\t\t\t\tScanned extensions recognition : 111 ms \n" + + "\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\tConfigurationHooksProcessedEvent : 111 ms \n" + + "\t\t\t\t\tBeforeInitEvent : 111 ms \n" + + "\t\t\t\t\tBundlesResolvedEvent : 111 ms \n" + + "\t\t\t\t\tBundlesInitializedEvent : 111 ms \n" + + "\t\t\t\t\tCommandsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInstallersResolvedEvent : 111 ms \n" + + "\t\t\t\t\tClasspathExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInitializedEvent : 111 ms \n" + + "\n" + + "\t\tDropwizard run : 111 ms \n" + + "\t\t\tConfiguration and Environment : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms \n" + + "\t\t\t\tConfiguration analysis : 111 ms \n" + + "\t\t\t\tGuicey bundles run : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms \n" + + "\t\t\t\tGuice modules processing : 111 ms \n" + + "\t\t\t\t\tBindings resolution : 111 ms \n" + + "\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tExtensions registration : 111 ms \n" + + "\t\t\t\t\tGuice bindings analysis : 111 ms \n" + + "\t\t\t\t\tExtensions installation : 111 ms \n" + + "\t\t\t\tInjector creation : 111 ms \n"); + + Assertions.assertThat(out).contains("\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\tBeforeRunEvent : 111 ms \n" + + "\t\t\t\t\tBundlesStartedEvent : 111 ms \n" + + "\t\t\t\t\tModulesAnalyzedEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsResolvedEvent : 111 ms \n" + + "\t\t\t\t\tInjectorCreationEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledByEvent : 111 ms \n" + + "\t\t\t\t\tExtensionsInstalledEvent : 111 ms \n" + + "\t\t\t\t\tApplicationRunEvent : 111 ms \n" + + "\n" + + "\t\tWeb server startup : 111 ms \n" + + "\t\t\tLifecycle simulation time : 111 ms \n" + + "\t\t\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\t\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\t\t\tmanaged DummyManaged : 111 ms \n" + + "\t\t\t\tGuicey time : 111 ms \n" + + "\t\t\t\t\tInstallers time : 111 ms \n" + + "\t\t\t\t\tListeners time : 111 ms \n" + + "\t\t\t\t\t\tApplicationStartingEvent : 111 ms \n" + + "\t\t\t\t\t\tJerseyConfigurationEvent : 111 ms \n" + + "\t\t\t\t\t\tJerseyExtensionsInstalledByEvent : 111 ms \n" + + "\t\t\t\t\t\tJerseyExtensionsInstalledEvent : 111 ms \n" + + "\t\t\t\t\t\tApplicationStartedEvent : 111 ms"); + + Assertions.assertThat(out).contains("Application shutdown time: \n" + + "\n" + + "\tApplication shutdown : 111 ms \n" + + "\t\tmanaged DummyManaged : 111 ms \n" + + "\t\tmanaged RegistryShutdown : 111 ms \n" + + "\t\tmanaged ExecutorServiceManager : 111 ms \n" + + "\t\tListeners time : 111 ms \n" + + "\t\t\tApplicationShutdownEvent : 111 ms \n" + + "\t\t\tApplicationStoppedEvent : 111 ms"); + } + + @Test + void testBundlesWarning() { + String out = run(Test5.class); + Assertions.assertThat(out).contains("Initialization time not tracked for bundles (move them after guice bundle to measure time): Bundle"); + + Assertions.assertThat(out).contains("\tApplication startup : 111 ms \n" + + "\t\tDropwizard initialization : 111 ms \n" + + "\t\t\tBundle : finished since start at 111 ms \n" + + "\t\t\tRecordedLogsTrackingBundle : finished since start at 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms (finished since start at 111 ms )"); + + Assertions.assertThat(out).contains("\t\tDropwizard run : 111 ms \n" + + "\t\t\tConfiguration and Environment : 111 ms \n" + + "\t\t\tBundle : 111 ms \n" + + "\t\t\tRecordedLogsTrackingBundle : 111 ms \n" + + "\t\t\tGuiceBundle : 111 ms "); + } + + @Test + void testAnonymousBundle() { + String out = run(Test6.class); + + Assertions.assertThat(out).contains("StartupDiagnosticTest$Test6$App$1 : finished since start at 111 ms "); + } + + @Test + void testAnonymousManaged() { + String out = run(Test7.class); + + Assertions.assertThat(out).contains("managed StartupDiagnosticTest$Test7$1 : 111 ms "); + } + + @Test + void testNoLifecycle() { + String out = run(Test8.class); + + // lifecycle still used, just managed objects not processed + Assertions.assertThat(out).contains("\t\tWeb server startup : 111 ms \n" + + "\t\t\tLifecycle simulation time : 111 ms \n" + + "\t\t\t\tGuicey time : 111 ms"); + } + + @Test + void testTransitiveBundles() { + String out = run(Test9.class); + Assertions.assertThat(out).contains("\t\t\t\tGuicey bundles init : 111 ms \n" + + "\t\t\t\t\tLastBundle : 111 ms \n" + + "\t\t\t\t\tMiddleBundle : 111 ms \n" + + "\t\t\t\t\tRootBundle : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms"); + + Assertions.assertThat(out).contains("\t\t\t\tGuicey bundles run : 111 ms \n" + + "\t\t\t\t\tLastBundle : 111 ms \n" + + "\t\t\t\t\tMiddleBundle : 111 ms \n" + + "\t\t\t\t\tRootBundle : 111 ms \n" + + "\t\t\t\t\tWebInstallersBundle : 111 ms \n" + + "\t\t\t\t\tCoreInstallersBundle : 111 ms"); + } + + @Disabled + @TestGuiceyApp(AutoScanApplication.class) + public static class Test1 { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.printStartupTime(); + + @Test + void test() { + } + } + + @Disabled + @TestDropwizardApp(AutoScanApplication.class) + public static class Test2 { + + @EnableHook + static GuiceyConfigurationHook hook = GuiceBundle.Builder::printStartupTime; + + @Test + void test() { + } + } + + @Disabled + @TestGuiceyApp(AutoScanApplication.class) + public static class Test3 { + + @EnableHook + static GuiceyConfigurationHook hook = GuiceBundle.Builder::printStartupTime; + + @StubRest + RestClient rest; + + @Test + void test() { + } + } + + @Disabled + @TestGuiceyApp(Test5.App.class) + public static class Test5 { + + @RecordLogs(DropwizardBundlesTracker.class) + RecordedLogs logs; + + @Test + void test() { + Assertions.assertThat(logs.containing("Initialization time not tracked for bundles").count()) + .isEqualTo(1); + } + + public static class App extends DefaultTestApp { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new Bundle()); + bootstrap.addBundle(GuiceBundle.builder().printStartupTime().build()); + } + } + + public static class Bundle implements ConfiguredBundle { + + } + } + + @Disabled + @TestGuiceyApp(Test6.App.class) + public static class Test6 { + + @Test + void test() { + } + + public static class App extends DefaultTestApp { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().printStartupTime().build()); + bootstrap.addBundle(new ConfiguredBundle() {}); + } + } + } + + @Disabled + @TestGuiceyApp(AutoScanApplication.class) + public static class Test7 { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder + .printStartupTime() + .onGuiceyStartup((config, env, injector) -> env.lifecycle().manage(new Managed() {})); + + @Test + void test() { + } + } + + + @Disabled + @TestGuiceyApp(value = DefaultTestApp.class, managedLifecycle = false) + public static class Test8 { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.printStartupTime(); + + @Test + void test() { + } + } + + @Disabled + @TestGuiceyApp(Test9.App.class) + public static class Test9 { + + @Test + void test() { + } + + public static class App extends DefaultTestApp { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new RootBundle()) + .printStartupTime().build()); + } + } + + public static class RootBundle implements GuiceyBundle { + @Override + public void initialize(GuiceyBootstrap bootstrap) { + bootstrap.bundles(new MiddleBundle()); + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + public static class MiddleBundle implements GuiceyBundle { + @Override + public void initialize(GuiceyBootstrap bootstrap) { + bootstrap.bundles(new LastBundle()); + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + public static class LastBundle implements GuiceyBundle { + @Override + public void initialize(GuiceyBootstrap bootstrap) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + + @Override + protected String clean(String out) { + return unifyLambdas(unifyMs(out)) + + // commands order may differ due to commands scan + .replace("DummyCommand : 111 ms", "NonInjactableCommand : 111 ms"); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy index 1b7a62061..328df2933 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/TwoDiagnosticReportsTogetherTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy index 203520798..313f9b46f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/WebMappingsReportTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.renderer.web.support.GuiceWebModule diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GenerifiedBindingsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GenerifiedBindingsTest.java new file mode 100644 index 000000000..3783ed655 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GenerifiedBindingsTest.java @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import com.google.inject.Inject; +import com.google.inject.TypeLiteral; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class GenerifiedBindingsTest extends AbstractPlatformTest { + + @Test + void testGenerifiedServices() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("Possible mistakes (unqualified JIT bindings):\n" + + "\n" + + "\t\t @Inject Service:\n" + + "\t\t\t instance [@Singleton] GenerifiedBindingsTest.Service : 111 ms \t\t ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:46)\n" + + "\t\t\t instance [@Singleton] GenerifiedBindingsTest.Service : 111 ms \t\t ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:45)\n" + + "\t\t\t> JIT [@Prototype] Service : 111 ms"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + @Test + void test() { + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Sample.class) + .modules(binder -> { + binder.bind(new TypeLiteral>() {}).toInstance(new Service<>() {}); + binder.bind(new TypeLiteral>() {}).toInstance(new Service<>() {}); + }) + .printGuiceProvisionTime() + .build(); + } + } + + public static class Service {} + + @EagerSingleton + public static class Sample { + + @Inject + Service serviceI; + @Inject + Service serviceS; + @Inject + Service service; + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GuiceProvisionDiagnosticTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GuiceProvisionDiagnosticTest.java new file mode 100644 index 000000000..f08a4eab4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/GuiceProvisionDiagnosticTest.java @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 24.03.2025 + */ +public class GuiceProvisionDiagnosticTest extends AbstractPlatformTest { + + @Test + void testDefaultOutput() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("Guice bindings provision time: \n" + + "\n" + + "\tOverall 50 provisions took 111 ms \n"); + Assertions.assertThat(out).contains( + "\t\tbinding [@Singleton] ManagedFilterPipeline : 111 ms \t\t com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:94)"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + + @Test + void test() { + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .printGuiceProvisionTime() + .build(); + } + } + + @Override + protected String clean(String out) { + return unifyMs(out).replaceAll("Overall \\d+", "Overall 50"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/IndirectProvisionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/IndirectProvisionTest.java new file mode 100644 index 000000000..82594fd99 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/IndirectProvisionTest.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class IndirectProvisionTest extends AbstractPlatformTest { + + @Test + void testIndirectProvisionCounter() { + String out = run(Test1.class); + Assertions.assertThat(out).contains( + "JIT [@Prototype] Service x2 : 111 ms (111 ms + 111 ms )"); + Assertions.assertThat(out).contains( + "providerinstance [@Prototype] Dep x2 : 111 ms (111 ms + 111 ms )"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + @Test + void test() { + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Ext1.class, Ext2.class) + .modules(binder -> binder.bind(Dep.class).toProvider((Provider) Dep::new)) + .printGuiceProvisionTime() + .build(); + } + } + + public static class Dep {} + + public static class Service { + @Inject + Dep dep; + } + + @EagerSingleton + public static class Ext1 { + @Inject + Service service; + } + + @EagerSingleton + public static class Ext2 { + @Inject + Service service; + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/ManyProvisionsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/ManyProvisionsTest.java new file mode 100644 index 000000000..ae7fa3d09 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/ManyProvisionsTest.java @@ -0,0 +1,52 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class ManyProvisionsTest extends AbstractPlatformTest { + + @Test + void testMultipleProvisions() { + String out = run(Test1.class); + Assertions.assertThat(out).contains( + "JIT [@Prototype] JitService x10 : 111 ms (111 ms + 111 ms + 111 ms + 111 ms + 111 ms + ...)"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + @Test + void test() { + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .printGuiceProvisionTime() + .onGuiceyStartup((config, env, injector) -> { + for (int i = 0; i < 10; i++) { + injector.getInstance(JitService.class); + } + }) + .build(); + } + } + + public static class JitService {} + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/MultipleProvisionsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/MultipleProvisionsTest.java new file mode 100644 index 000000000..b9a010f4e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/MultipleProvisionsTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class MultipleProvisionsTest extends AbstractPlatformTest { + + @Test + void testMultipleProvisions() { + String out = run(Test1.class); + Assertions.assertThat(out).contains( + "JIT [@Prototype] JitService x2 : 111 ms (111 ms + 111 ms )"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + @Test + void test() { + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Service1.class, Service2.class) + .printGuiceProvisionTime() + .build(); + } + } + + public static class JitService {} + + @EagerSingleton + public static class Service1 { + + @Inject + JitService service; + } + + @EagerSingleton + public static class Service2 { + + @Inject + JitService service; + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/StandaloneUsageTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/StandaloneUsageTest.java new file mode 100644 index 000000000..af86c65a9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/StandaloneUsageTest.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import com.google.inject.Injector; +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.debug.hook.GuiceProvisionTimeHook; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class StandaloneUsageTest { + + @EnableHook + static GuiceProvisionTimeHook report = new GuiceProvisionTimeHook(); + + @Inject + Injector injector; + + @Test + void testRuntimeReport() { + + report.clearData(); + injector.getInstance(Service.class); + injector.getInstance(Service.class); + + Assertions.assertThat(report.getRecordedData().keys().size()).isEqualTo(2); + + String res = report.renderReport(); + System.out.println(res); + + Assertions.assertThat(res).contains( + "JIT [@Prototype] Service x2"); + } + + public static class Service {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/WrongConfigUsageTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/WrongConfigUsageTest.java new file mode 100644 index 000000000..3d7863e81 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/provision/WrongConfigUsageTest.java @@ -0,0 +1,118 @@ +package ru.vyarus.dropwizard.guice.debug.provision; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.inject.BindingAnnotation; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +public class WrongConfigUsageTest extends AbstractPlatformTest { + + @Test + void testWrongUsageDetection() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("Possible mistakes (unqualified JIT bindings):\n" + + "\n" + + "\t\t @Inject Sub:\n" + + "\t\t\t instance [@Singleton] @Config(\"val2\") Sub : 111 ms \t\t ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135)\n" + + "\t\t\t instance [@Singleton] @Marker Sub : 111 ms \t\t ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindCustomQualifiers(ConfigBindingModule.java:93)\n" + + "\t\t\t> JIT [@Prototype] Sub : 111 ms \t\t \n" + + "\n" + + "\t\t @Inject Uniq:\n" + + "\t\t\t instance [@Singleton] @Config Uniq : 111 ms \t\t ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123)\n" + + "\t\t\t> JIT [@Prototype] Uniq : 111 ms"); + } + + @TestGuiceyApp(App.class) + @Disabled + public static class Test1 { + + @Test + void test() { + } + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(Service.class) + .printGuiceProvisionTime() + .build()); + } + + @Override + public void run(Config configuration, Environment environment) throws Exception { + } + } + + public static class Config extends Configuration { + // not unique but one with custom annotation + @Marker + private Sub val = new Sub(); + + private Sub val2 = new Sub(); + + private Uniq uniq = new Uniq(); + + public Sub getVal() { + return val; + } + + public Sub getVal2() { + return val2; + } + + public Uniq getUniq() { + return uniq; + } + } + + public static class Sub { + @JsonProperty + String val; + } + + public static class Uniq { + @JsonProperty + String val; + } + + @Retention(RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) + @BindingAnnotation + public @interface Marker {} + + @EagerSingleton + public static class Service { + @Inject + Sub val; + @Inject + Uniq uniq; + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy similarity index 99% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy index 3e964cc89..6c8ebc1df 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ContextTreeRendererTest.groovy @@ -4,10 +4,10 @@ import com.google.common.collect.Lists import com.google.inject.AbstractModule import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup import ru.vyarus.dropwizard.guice.debug.report.tree.ContextTreeConfig @@ -27,8 +27,8 @@ import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path import static ru.vyarus.dropwizard.guice.module.context.ConfigScope.Hook import static ru.vyarus.dropwizard.guice.module.context.ConfigScope.allExcept diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy index 0555711ff..e6bdcc308 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/DiagnosticRendererTest.groovy @@ -3,11 +3,11 @@ package ru.vyarus.dropwizard.guice.debug.renderer import com.google.common.collect.Lists import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup import ru.vyarus.dropwizard.guice.debug.report.diagnostic.DiagnosticConfig @@ -27,8 +27,8 @@ import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ExtensionsHelpRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ExtensionsHelpRendererTest.groovy new file mode 100644 index 000000000..553f58384 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/ExtensionsHelpRendererTest.groovy @@ -0,0 +1,142 @@ +package ru.vyarus.dropwizard.guice.debug.renderer + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.report.extensions.ExtensionsHelpRenderer +import ru.vyarus.dropwizard.guice.diagnostic.BaseDiagnosticTest +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.InstallersResolvedEvent +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 12.12.2022 + */ +@TestGuiceyApp(App) +class ExtensionsHelpRendererTest extends BaseDiagnosticTest { + + @Inject + Bootstrap bootstrap + + def "Check extensions help render"() { + + expect: + new ExtensionsHelpRenderer((bootstrap.getApplication() as App).installers) + .renderReport(null).replaceAll("\r", "").replaceAll(" +\n", "\n") == """ + + lifecycle (r.v.d.g.m.i.f.LifeCycleInstaller) + implements LifeCycle + + managed (r.v.d.g.m.i.feature.ManagedInstaller) + implements Managed + + jerseyfeature (r.v.d.g.m.i.f.j.JerseyFeatureInstaller) + implements Feature + + jerseyprovider (r.v.d.g.m.i.f.j.p.JerseyProviderInstaller) + @Provider on class + implements ExceptionMapper + implements ParamConverterProvider + implements ContextResolver + implements MessageBodyReader + implements MessageBodyWriter + implements ReaderInterceptor + implements WriterInterceptor + implements ContainerRequestFilter + implements ContainerResponseFilter + implements DynamicFeature + implements ValueParamProvider + implements InjectionResolver + implements ApplicationEventListener + implements ModelProcessor + + resource (r.v.d.g.m.i.f.j.ResourceInstaller) + @Path on class + @Path on implemented interface + + eagersingleton (r.v.d.g.m.i.f.e.EagerSingletonInstaller) + @EagerSingleton on class + + healthcheck (r.v.d.g.m.i.f.h.HealthCheckInstaller) + extends NamedHealthCheck + + task (r.v.d.g.m.i.feature.TaskInstaller) + extends Task + + plugin (r.v.d.g.m.i.f.plugin.PluginInstaller) + @Plugin on class + custom annotation on class, annotated with @Plugin + + webservlet (r.v.d.g.m.i.f.w.WebServletInstaller) + extends HttpServlet + @WebServlet + + webfilter (r.v.d.g.m.i.f.web.WebFilterInstaller) + implements Filter + @WebFilter + + weblistener (r.v.d.g.m.i.f.w.l.WebListenerInstaller) + implements EventListener + @WebListener + + custom (r.v.d.g.d.r.ExtensionsHelpRendererTest\$CustomInstaller) + sign 1 + sign 2 + + custom2 (r.v.d.g.d.r.ExtensionsHelpRendererTest\$CustomInstaller2) + +""" + } + + static class App extends Application { + + List installers + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .installers(CustomInstaller, CustomInstaller2) + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void installersResolved(InstallersResolvedEvent event) { + installers = event.installers + } + }) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class CustomInstaller implements FeatureInstaller { + @Override + boolean matches(Class type) { + return false + } + + @Override + void report() { + } + + @Override + List getRecognizableSigns() { + return Arrays.asList("sign 1", "sign 2") + } + } + + static class CustomInstaller2 implements FeatureInstaller { + @Override + boolean matches(Class type) { + return false + } + + @Override + void report() { + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy index b2efe51f7..b5a7f9c21 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/OptionsRendererTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions import ru.vyarus.dropwizard.guice.debug.report.option.OptionsConfig @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -48,6 +48,7 @@ class OptionsRendererTest extends Specification { Guicey (r.v.dropwizard.guice.GuiceyOptions) ScanPackages = [com.foo, com.bat] *CUSTOM + ScanProtectedClasses = false SearchCommands = false UseCoreInstallers = true BindConfigurationByPath = true @@ -82,6 +83,7 @@ class OptionsRendererTest extends Specification { Guicey (r.v.dropwizard.guice.GuiceyOptions) ScanPackages = [com.foo, com.bat] *CUSTOM + ScanProtectedClasses = false SearchCommands = false UseCoreInstallers = true BindConfigurationByPath = true @@ -135,6 +137,7 @@ class OptionsRendererTest extends Specification { Guicey (r.v.dropwizard.guice.GuiceyOptions) ScanPackages = [com.foo, com.bat] *CUSTOM + ScanProtectedClasses = false SearchCommands = false UseCoreInstallers = true BindConfigurationByPath = true diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy index 0edc573ea..49b910524 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRenderWithDwBundleTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.stat.StatsRenderer import ru.vyarus.dropwizard.guice.diagnostic.BaseDiagnosticTest @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.LifeCycleInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy index 6ada473c7..9f984d9b3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererFullTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.stat.StatsRenderer import ru.vyarus.dropwizard.guice.diagnostic.BaseDiagnosticTest @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.LifeCycleInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy index b890018dd..f27c89f56 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/StatRendererTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.stat.StatsRenderer import ru.vyarus.dropwizard.guice.diagnostic.BaseDiagnosticTest @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.installer.feature.LifeCycleInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy index 63512c107..43fa17843 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererForSpecialMethodsTest.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.AbstractModule import com.google.inject.Injector import com.google.inject.matcher.Matchers -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.aopalliance.intercept.MethodInterceptor import org.aopalliance.intercept.MethodInvocation import ru.vyarus.dropwizard.guice.GuiceBundle @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceAopMapRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -40,12 +40,10 @@ class GuiceAopRendererForSpecialMethodsTest extends Specification { def "Check aop render"() { expect: - render(new GuiceAopConfig()) - // appears when running from IDEA - .replace("org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:318)", "sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)") == """ + render(new GuiceAopConfig()) == """ 1 AOP handlers declared - └── GuiceAopRendererForSpecialMethodsTest\$App\$1/GuiceAopRendererForSpecialMethodsTest\$App\$1\$1 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + └── GuiceAopRendererForSpecialMethodsTest\$App\$1/GuiceAopRendererForSpecialMethodsTest\$App\$1\$1 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) 1 bindings affected by AOP diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy index f4687ec1b..04bffc8fe 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceAopRendererTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector import com.google.inject.matcher.Matchers -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.AopModule @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceAopMapRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -67,6 +67,7 @@ class GuiceAopRendererTest extends Specification { │ ├── getExtensionsRegisteredManually() Interceptor1 │ ├── getGuiceyBundleIds() Interceptor1 │ ├── getGuiceyBundles() Interceptor1 + │ ├── getGuiceyBundlesInInitOrder() Interceptor1 │ ├── getInfo(Class) Interceptor1 │ ├── getInfos(Class) Interceptor1 │ ├── getInstallers() Interceptor1 @@ -146,6 +147,7 @@ class GuiceAopRendererTest extends Specification { │ ├── getExtensionsRegisteredManually() Interceptor1 │ ├── getGuiceyBundleIds() Interceptor1 │ ├── getGuiceyBundles() Interceptor1 + │ ├── getGuiceyBundlesInInitOrder() Interceptor1 │ ├── getInfos(Class) Interceptor1 │ ├── getInstallers() Interceptor1 │ ├── getInstallersDisabled() Interceptor1 diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy index c45a47c8b..865716be4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererCasesTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.CasesModule @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -87,33 +87,33 @@ class GuiceRendererCasesTest extends Specification { │ └── instance [@Singleton] BindService2 at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.CasesModule.configure(CasesModule.java:38) *OVERRIDDEN │ └── GuiceBootstrapModule (r.v.d.guice.module) - ├── [@Prototype] - at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:51) - ├── instance [@Singleton] Options at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:57) - ├── instance [@Singleton] ConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:60) - ├── instance [@Singleton] StatsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:61) - ├── instance [@Singleton] OptionsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:62) - ├── untargetted [@Singleton] GuiceyConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:63) - ├── instance [@Singleton] Bootstrap at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:71) - ├── instance [@Singleton] Environment at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:72) + ├── [@Prototype] - at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:56) + ├── instance [@Singleton] Options at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:62) + ├── instance [@Singleton] ConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:65) + ├── instance [@Singleton] StatsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:66) + ├── instance [@Singleton] OptionsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:67) + ├── untargetted [@Singleton] GuiceyConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:68) + ├── instance [@Singleton] Bootstrap at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:76) + ├── instance [@Singleton] Environment at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:77) │ ├── InstallerModule (r.v.d.g.m.installer) - │ └── instance [@Singleton] ExtensionsHolder at ru.vyarus.dropwizard.guice.module.installer.InstallerModule.configure(InstallerModule.java:30) + │ └── instance [@Singleton] ExtensionsHolder at ru.vyarus.dropwizard.guice.module.installer.InstallerModule.configure(InstallerModule.java:35) │ └── Jersey2Module (r.v.d.g.m.jersey) - ├── providerinstance [@Prototype] InjectionManager at ru.vyarus.dropwizard.guice.module.jersey.Jersey2Module.configure(Jersey2Module.java:59) + ├── providerinstance [@Prototype] InjectionManager at ru.vyarus.dropwizard.guice.module.jersey.Jersey2Module.configure(Jersey2Module.java:68) ├── GuiceWebModule (r.v.d.g.m.jersey) *WEB │ └── GuiceBindingsModule (r.v.d.g.m.jersey.hk2) - ├── providerinstance [@Prototype] Application at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@Prototype] MultivaluedParameterExtractorProvider at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@Prototype] Providers at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] AsyncContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] ContainerRequest at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] HttpHeaders at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] Request at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] ResourceInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - ├── providerinstance [@RequestScoped] SecurityContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - └── providerinstance [@RequestScoped] UriInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) + ├── providerinstance [@Prototype] Application at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@Prototype] MultivaluedParameterExtractorProvider at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@Prototype] Providers at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] AsyncContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] ContainerRequest at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] HttpHeaders at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] Request at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] ResourceInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + ├── providerinstance [@RequestScoped] SecurityContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + └── providerinstance [@RequestScoped] UriInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) 1 OVERRIDING MODULES with 2 bindings @@ -138,7 +138,7 @@ class GuiceRendererCasesTest extends Specification { expect: render(new GuiceConfig()) == """ - 8 MODULES with 111 bindings + 8 MODULES with 105 bindings │ ├── CasesModule (r.v.d.g.d.r.g.support) │ ├── CustomTypeListener at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.CasesModule.configure(CasesModule.java:19) @@ -149,20 +149,20 @@ class GuiceRendererCasesTest extends Specification { │ └── instance [@Singleton] BindService2 at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.CasesModule.configure(CasesModule.java:38) *OVERRIDDEN │ └── GuiceBootstrapModule (r.v.d.guice.module) - ├── [@Prototype] - at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:51) - ├── instance [@Singleton] Options at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:57) - ├── instance [@Singleton] ConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:60) - ├── instance [@Singleton] StatsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:61) - ├── instance [@Singleton] OptionsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:62) - ├── untargetted [@Singleton] GuiceyConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:63) - ├── instance [@Singleton] Bootstrap at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:71) - ├── instance [@Singleton] Environment at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:72) + ├── [@Prototype] - at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:56) + ├── instance [@Singleton] Options at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:62) + ├── instance [@Singleton] ConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:65) + ├── instance [@Singleton] StatsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:66) + ├── instance [@Singleton] OptionsInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:67) + ├── untargetted [@Singleton] GuiceyConfigurationInfo at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:68) + ├── instance [@Singleton] Bootstrap at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:76) + ├── instance [@Singleton] Environment at ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:77) │ ├── InstallerModule (r.v.d.g.m.installer) - │ └── instance [@Singleton] ExtensionsHolder at ru.vyarus.dropwizard.guice.module.installer.InstallerModule.configure(InstallerModule.java:30) + │ └── instance [@Singleton] ExtensionsHolder at ru.vyarus.dropwizard.guice.module.installer.InstallerModule.configure(InstallerModule.java:35) │ ├── Jersey2Module (r.v.d.g.m.jersey) - │ ├── providerinstance [@Prototype] InjectionManager at ru.vyarus.dropwizard.guice.module.jersey.Jersey2Module.configure(Jersey2Module.java:59) + │ ├── providerinstance [@Prototype] InjectionManager at ru.vyarus.dropwizard.guice.module.jersey.Jersey2Module.configure(Jersey2Module.java:68) │ │ │ ├── GuiceWebModule (r.v.d.g.m.jersey) *WEB │ │ │ @@ -183,95 +183,89 @@ class GuiceRendererCasesTest extends Specification { │ │ └── providerinstance [@RequestScoped] @RequestParameters Map at com.google.inject.servlet.InternalServletModule.provideRequestParameters(InternalServletModule.java:131) │ │ │ └── GuiceBindingsModule (r.v.d.g.m.jersey.hk2) - │ ├── providerinstance [@Prototype] Application at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@Prototype] MultivaluedParameterExtractorProvider at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@Prototype] Providers at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] AsyncContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] ContainerRequest at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] HttpHeaders at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] Request at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] ResourceInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ ├── providerinstance [@RequestScoped] SecurityContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) - │ └── providerinstance [@RequestScoped] UriInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:190) + │ ├── providerinstance [@Prototype] Application at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@Prototype] MultivaluedParameterExtractorProvider at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@Prototype] Providers at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] AsyncContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] ContainerRequest at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] HttpHeaders at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] Request at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] ResourceInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ ├── providerinstance [@RequestScoped] SecurityContext at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) + │ └── providerinstance [@RequestScoped] UriInfo at ru.vyarus.dropwizard.guice.module.installer.util.JerseyBinding.bindJerseyComponent(JerseyBinding.java:197) │ └── ConfigBindingModule (r.v.d.g.m.yaml.bind) - ├── instance [@Singleton] ConfigurationTree at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.configure(ConfigBindingModule.java:45) - ├── instance [@Singleton] Configuration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:63) - ├── instance [@Singleton] @Config Configuration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:65) - ├── instance [@Singleton] @Config AdminFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config GzipHandlerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config HealthCheckConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config LoggingFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config MetricsFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config RequestLogFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config ServerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config ServerPushFilterFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config TaskConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:78) - ├── instance [@Singleton] @Config("admin") AdminFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.healthChecks") HealthCheckConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.healthChecks.maxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.healthChecks.minThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.healthChecks.servletEnabled") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.healthChecks.workQueueSize") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.tasks") TaskConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("admin.tasks.printStackTraceOnError") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("health") Optional at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("logging") LoggingFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("logging.appenders") List> at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("logging.level") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("logging.loggers") Map at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("metrics") MetricsFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("metrics.frequency") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("metrics.reportOnStop") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("metrics.reporters") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server") ServerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.adminConnectors") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.adminContextPath") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.adminMaxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.adminMinThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.allowedMethods") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.applicationConnectors") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.applicationContextPath") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.detailedJsonProcessingExceptionMapper") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.dumpAfterStart") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.dumpBeforeStop") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.enableThreadNameFilter") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip") GzipHandlerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.bufferSize") DataSize at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.deflateCompressionLevel") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.enabled") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.excludedUserAgentPatterns") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.gzipCompatibleInflation") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.minimumEntitySize") DataSize at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.gzip.syncFlush") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.idleThreadTimeout") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.maxQueuedRequests") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.maxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.minThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.registerDefaultExceptionMappers") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.requestLog") RequestLogFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.requestLog.appenders") List> at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.rootPath") Optional at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.serverPush") ServerPushFilterFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.serverPush.associatePeriod") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.serverPush.enabled") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.serverPush.maxAssociations") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── instance [@Singleton] @Config("server.shutdownGracePeriod") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gid") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.group") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gzip.compressedMimeTypes") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gzip.excludedMimeTypes") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gzip.excludedPaths") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gzip.includedMethods") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.gzip.includedPaths") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.nofileHardLimit") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.nofileSoftLimit") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.serverPush.refererHosts") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.serverPush.refererPorts") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.startsAsRoot") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.uid") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - ├── providerinstance [@Prototype] @Config("server.umask") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) - └── providerinstance [@Prototype] @Config("server.user") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:90) + ├── instance [@Singleton] ConfigurationTree at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.configure(ConfigBindingModule.java:58) + ├── instance [@Singleton] Configuration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:108) + ├── instance [@Singleton] @Config Configuration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:110) + ├── instance [@Singleton] @Config AdminFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config GzipHandlerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config HealthCheckConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config LoggingFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config MetricsFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config RequestLogFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config ServerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config TaskConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:123) + ├── instance [@Singleton] @Config("admin") AdminFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.healthChecks") HealthCheckConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.healthChecks.maxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.healthChecks.minThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.healthChecks.servletEnabled") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.healthChecks.workQueueSize") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.tasks") TaskConfiguration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("admin.tasks.printStackTraceOnError") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("health") Optional at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("logging") LoggingFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("logging.appenders") List> at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("logging.level") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("logging.loggers") Map at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("metrics") MetricsFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("metrics.frequency") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("metrics.reportOnStop") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("metrics.reporters") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server") ServerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.adminConnectors") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.adminContextPath") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.adminMaxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.adminMinThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.allowedMethods") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.applicationConnectors") List at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.applicationContextPath") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.detailedJsonProcessingExceptionMapper") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.dumpAfterStart") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.dumpBeforeStop") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.enableAdminVirtualThreads") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.enableThreadNameFilter") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.enableVirtualThreads") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip") GzipHandlerFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip.bufferSize") DataSize at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip.deflateCompressionLevel") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip.enabled") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip.minimumEntitySize") DataSize at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.gzip.syncFlush") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.idleThreadTimeout") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.maxThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.minThreads") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.registerDefaultExceptionMappers") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.requestLog") RequestLogFactory at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.requestLog.appenders") List> at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.responseMeteredLevel") ResponseMeteredLevel at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.rootPath") Optional at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── instance [@Singleton] @Config("server.shutdownGracePeriod") Duration at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gid") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.group") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gzip.compressedMimeTypes") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gzip.excludedMimeTypes") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gzip.excludedPaths") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gzip.includedMethods") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.gzip.includedPaths") Set at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.metricPrefix") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.nofileHardLimit") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.nofileSoftLimit") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.startsAsRoot") Boolean at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.uid") Integer at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + ├── providerinstance [@Prototype] @Config("server.umask") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) + └── providerinstance [@Prototype] @Config("server.user") String at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:135) 1 OVERRIDING MODULES with 2 bindings diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy index 295c91580..dfa8ca5ed 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererConstantTest.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector import com.google.inject.Key import com.google.inject.name.Names -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.ConstantModule @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy index 7ceb0ad9e..5b0148051 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererDisabledModuleNoAnalysisTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy index 0b175438f..189832948 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererMultibindingsTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.MultibindingsModule @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy index 60c59cdbf..02dcac1d9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererPrivateModuleTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.OuterModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -41,20 +41,28 @@ class GuiceRendererPrivateModuleTest extends Specification { .hideGuiceBindings() .hideGuiceyBindings()) == """ - 4 MODULES with 4 bindings + 4 MODULES with 6 bindings │ └── OuterModule (r.v.d.g.d.r.g.s.privt) │ └── InnerModule (r.v.d.g.d.r.g.s.privt) *PRIVATE - ├── untargetted [@Prototype] InnerService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:14) - ├── untargetted [@Prototype] OuterService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:15) *EXPOSED - ├── exposed [@Prototype] OuterService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:17) + ├── untargetted [@Prototype] InnerService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:16) + ├── untargetted [@Prototype] OuterService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:17) *EXPOSED + ├── linkedkey [@Prototype] OService --> IndirectOuterService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:19) *EXPOSED + ├── exposed [@Prototype] OuterService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:21) + ├── exposed [@Prototype] OService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.configure(InnerModule.java:22) + ├── providerinstance [@Prototype] OuterProviderService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.getService(InnerModule.java:27) *EXPOSED + ├── exposed [@Prototype] OuterProviderService at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.InnerModule.getService(InnerModule.java:27) │ └── Inner2Module (r.v.d.g.d.r.g.s.privt) ├── untargetted [@Prototype] InnerService2 at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.Inner2Module.configure(Inner2Module.java:14) │ └── Inner3Module (r.v.d.g.d.r.g.s.privt) *PRIVATE └── untargetted [@Prototype] OutServ at ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt.Inner3Module.configure(Inner3Module.java:13) *EXPOSED + + + BINDING CHAINS + └── OService --[linked]--> IndirectOuterService """ as String; } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy index 2dbe8d1fd..6d48906fc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererProviderMethodTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.ProviderMethodModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy index ca2cd92be..7a1b9ac66 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedBindingTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.DisableExtensionModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy index 90a1e863f..ccb5407c0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererRemovedModuleTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.TransitiveModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererSingletonDetectionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererSingletonDetectionTest.groovy new file mode 100644 index 000000000..23be3a04c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererSingletonDetectionTest.groovy @@ -0,0 +1,143 @@ +package ru.vyarus.dropwizard.guice.debug.renderer.guice + +import com.google.inject.AbstractModule +import com.google.inject.Injector +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import jakarta.inject.Singleton +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup +import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceBindingsRenderer +import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig +import ru.vyarus.dropwizard.guice.module.support.scope.Prototype +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 14.11.2024 + */ +@TestDropwizardApp(App) +class GuiceRendererSingletonDetectionTest extends Specification { + + static { + System.clearProperty(PropertyBundleLookup.BUNDLES_PROPERTY) + } + + @Inject + Injector injector + GuiceBindingsRenderer renderer + + void setup() { + renderer = new GuiceBindingsRenderer(injector) + } + + def "Check singleton detection"() { + + expect: + render(new GuiceConfig() + .hideGuiceBindings() + .hideGuiceyBindings()) == """ + + 1 MODULES with 12 bindings + │ + └── Module (r.v.d.g.d.r.g.GuiceRendererSingletonDetectionTest) + ├── linkedkey [@Prototype] Bind4 --> Ext4 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Prototype] Bind5 --> Ext5 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Prototype] LongBind --> LongExt1 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Singleton] Bind1 --> Ext1 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Singleton] Bind2 --> Ext2 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Singleton] Bind3 --> Ext3 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── linkedkey [@Singleton] LongExt1 --> LongExt2 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── untargetted [@Prototype] Simple4 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── untargetted [@Prototype] Simple5 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── untargetted [@Singleton] Simple1 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + ├── untargetted [@Singleton] Simple2 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + └── untargetted [@Singleton] Simple3 at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:321) + + + BINDING CHAINS + ├── Bind1 --[linked]--> Ext1 + ├── Bind2 --[linked]--> Ext2 + ├── Bind3 --[linked]--> Ext3 + ├── Bind4 --[linked]--> Ext4 + ├── Bind5 --[linked]--> Ext5 + └── LongBind --[linked]--> LongExt1 --[linked]--> LongExt2 +""" as String + } + + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printGuiceBindings() + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends AbstractModule { + @Override + protected void configure() { + bind(Simple1) + bind(Simple2).in(Singleton.class) + bind(Simple3).asEagerSingleton() + bind(Simple4) + bind(Simple5) + + // linked + bind(Bind1).to(Ext1) + bind(Bind2).to(Ext2).in(Singleton.class) + bind(Bind3).to(Ext3).asEagerSingleton() + bind(Bind4).to(Ext4) + bind(Bind5).to(Ext5) + + bind(LongBind).to(LongExt1) + bind(LongExt1).to(LongExt2) + } + } + + @Singleton + static class Simple1 {} + static class Simple2 {} + static class Simple3 {} + static class Simple4 {} + @Prototype + static class Simple5 {} + + static interface Bind1 {} + @Singleton + static class Ext1 implements Bind1 {} + + static interface Bind2 {} + static class Ext2 implements Bind2 {} + + static interface Bind3 {} + static class Ext3 implements Bind3 {} + + static interface Bind4 {} + static class Ext4 implements Bind4 {} + + static interface Bind5 {} + @Prototype + static class Ext5 implements Bind5 {} + + + static interface LongBind {} + static class LongExt1 implements LongBind {} + @Singleton + static class LongExt2 extends LongExt1 {} + + String render(GuiceConfig config) { + renderer.renderReport(config).replaceAll("\r", "").replaceAll(" +\n", "\n") + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy index 940604657..470428510 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/GuiceRendererWebModuleTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.guice.support.WebModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.GuiceConfig import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/AopModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/AopModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/AopModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/AopModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/CasesModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/CasesModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/CasesModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/CasesModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ConstantModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ConstantModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ConstantModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ConstantModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java index a31b26edc..2885d2000 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/DisableExtensionModule.java @@ -3,7 +3,7 @@ import com.google.inject.AbstractModule; import com.google.inject.Provider; -import javax.ws.rs.Path; +import jakarta.ws.rs.Path; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/MultibindingsModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/MultibindingsModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/MultibindingsModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/MultibindingsModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/OverrideModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/OverrideModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/OverrideModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/OverrideModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ProviderMethodModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ProviderMethodModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ProviderMethodModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/ProviderMethodModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java index e3bdb01b8..7ef43ff17 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/TransitiveModule.java @@ -2,7 +2,7 @@ import com.google.inject.AbstractModule; -import javax.ws.rs.Path; +import jakarta.ws.rs.Path; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java index d022e9c77..896707a16 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/WebModule.java @@ -3,8 +3,8 @@ import com.google.inject.Singleton; import com.google.inject.servlet.ServletModule; -import javax.servlet.http.HttpFilter; -import javax.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServlet; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java index 957e3701c..20853e907 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/AopedService.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice.support.exts; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService2.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService2.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService2.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/BindService2.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomAop.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomAop.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomAop.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomAop.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomProvisionListener.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomProvisionListener.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomProvisionListener.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomProvisionListener.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomTypeListener.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomTypeListener.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomTypeListener.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/CustomTypeListener.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java index 5b92fe524..497f5af9d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/JitService.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice.support.exts; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverriddenService.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverriddenService.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverriddenService.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverriddenService.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverrideService.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverrideService.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverrideService.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/exts/OverrideService.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner2Module.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner2Module.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner2Module.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner2Module.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner3Module.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner3Module.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner3Module.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/Inner3Module.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java similarity index 53% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java index 1bb9b9617..baeb9a5d5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/InnerModule.java @@ -1,6 +1,8 @@ package ru.vyarus.dropwizard.guice.debug.renderer.guice.support.privt; +import com.google.inject.Exposed; import com.google.inject.PrivateModule; +import com.google.inject.Provides; /** * @author Vyacheslav Rusakov @@ -14,9 +16,22 @@ protected void configure() { bind(InnerService.class); bind(OuterService.class); + bind(OService.class).to(IndirectOuterService.class); + expose(OuterService.class); + expose(OService.class); + } + + @Provides @Exposed + public OuterProviderService getService() { + return new OuterProviderService(); } public static class InnerService {} public static class OuterService {} + + public static class OuterProviderService {} + + public interface OService {} + public static class IndirectOuterService implements OService {} } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/OuterModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/OuterModule.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/OuterModule.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/support/privt/OuterModule.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy similarity index 99% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy index 410072929..c2eb718d3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/GuiceModelTest.groovy @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.debug.report.guice.model.ModuleDeclaration import ru.vyarus.dropwizard.guice.debug.report.guice.util.GuiceModelParser import spock.lang.Specification -import javax.inject.Provider +import jakarta.inject.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/MultipleModuleInstancesParseTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/MultipleModuleInstancesParseTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/MultipleModuleInstancesParseTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/MultipleModuleInstancesParseTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy index dc0acae81..50adfaab0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/guice/util/ScopingVisitorTest.groovy @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton import ru.vyarus.dropwizard.guice.module.support.scope.Prototype import spock.lang.Specification -import javax.inject.Singleton +import jakarta.inject.Singleton import java.lang.annotation.Annotation /** @@ -69,7 +69,7 @@ class ScopingVisitorTest extends Specification { def "Check direct visitor cases"() { expect: "correct scope annotations" - visitor.visitNoScoping() == Prototype + visitor.visitNoScoping() == null visitor.visitEagerSingleton() == EagerSingleton visitor.visitScope(Scopes.SINGLETON) == Singleton @@ -85,12 +85,12 @@ class ScopingVisitorTest extends Specification { } Class scope(Injector injector, Class type) { - injector.getExistingBinding(Key.get(type)).acceptScopingVisitor(visitor) + visitor.performDetection(injector.getExistingBinding(Key.get(type))) } Class scope(List elements, Class type) { - (elements.find { it instanceof Binding && (it as Binding).getKey().getTypeLiteral().getRawType() == type } as Binding) - .acceptScopingVisitor(visitor) + visitor.performDetection( + elements.find { it instanceof Binding && (it as Binding).getKey().getTypeLiteral().getRawType() == type } as Binding) } static class Module extends AbstractModule { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy index b6b3374a3..a44dcda79 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/CompleteRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -38,7 +38,8 @@ class CompleteRenderTest extends Specification { render(new JerseyConfig()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IOException GuiceExceptionMapper (r.v.d.g.c.h.support) @@ -66,6 +67,7 @@ class CompleteRenderTest extends Specification { byte[] ByteArrayProvider (o.g.j.m.internal) [application/octet-stream, */*] DataSource DataSourceProvider (o.g.j.m.internal) [application/octet-stream, */*] Document DocumentProvider (o.g.j.jaxb.internal) [application/xml, text/xml, */*] + List EntityPartReader (o.g.j.m.m.internal) [multipart/form-data] Enum EnumMessageProvider (o.g.j.m.internal) [text/plain] File FileProvider (o.g.j.m.internal) [application/octet-stream, */*] MultivaluedMap FormMultivaluedMapProvider (o.g.j.m.internal) [application/x-www-form-urlencoded] @@ -73,10 +75,13 @@ class CompleteRenderTest extends Specification { Type GuiceMessageBodyReader (r.v.d.g.c.h.support) Type HKMessageBodyReader (r.v.d.g.c.h.s.hk) *jersey managed InputStream InputStreamProvider (o.g.j.m.internal) [application/octet-stream, */*] - Object JacksonJsonProvider (c.f.j.jaxrs.json) [*/*] + Object JacksonJsonProvider (c.f.j.j.rs.json) [*/*] Object JacksonMessageBodyProvider (i.d.jersey.jackson) [*/*] + MultiPart MultiPartReaderServerSide (o.g.j.m.m.internal) [multipart/*] + Path PathProvider (o.g.j.m.internal) [application/octet-stream, */*] Reader ReaderProvider (o.g.j.m.internal) [text/plain, */*] RenderedImage RenderedImageProvider (o.g.j.m.internal) [image/*, application/octet-stream] + EntityPart SingleEntityPartReader (o.g.j.m.m.internal) DOMSource DomSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] SAXSource SaxSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] StreamSource StreamSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] @@ -100,6 +105,7 @@ class CompleteRenderTest extends Specification { ChunkedOutput ChunkedResponseWriter (o.g.jersey.server) DataSource DataSourceProvider (o.g.j.m.internal) [application/octet-stream, */*] Document DocumentProvider (o.g.j.jaxb.internal) [application/xml, text/xml, */*] + List EntityPartWriter (o.g.j.m.m.internal) [multipart/form-data] Enum EnumMessageProvider (o.g.j.m.internal) [text/plain] File FileProvider (o.g.j.m.internal) [application/octet-stream, */*] MultivaluedMap FormMultivaluedMapProvider (o.g.j.m.internal) [application/x-www-form-urlencoded] @@ -107,15 +113,18 @@ class CompleteRenderTest extends Specification { Type GuiceMessageBodyWriter (r.v.d.g.c.h.support) Type HKMessageBodyWriter (r.v.d.g.c.h.s.hk) *jersey managed InputStream InputStreamProvider (o.g.j.m.internal) [application/octet-stream, */*] - Object JacksonJsonProvider (c.f.j.jaxrs.json) [*/*] - Object JacksonMessageBodyProvider (i.d.jersey.jackson) [*/*] + Object JacksonJsonProvider (c.f.j.j.rs.json) [application/json, text/json, */*] + Object JacksonMessageBodyProvider (i.d.jersey.jackson) [application/json, text/json, */*] + MultiPart MultiPartWriter (o.g.j.m.m.internal) [multipart/*] OptionalDouble OptionalDoubleMessageBodyWriter (i.d.jersey.optional) [*/*] OptionalInt OptionalIntMessageBodyWriter (i.d.jersey.optional) [*/*] OptionalLong OptionalLongMessageBodyWriter (i.d.jersey.optional) [*/*] Optional OptionalMessageBodyWriter (i.d.jersey.guava) [*/*] Optional OptionalMessageBodyWriter (i.d.jersey.optional) [*/*] + Path PathProvider (o.g.j.m.internal) [application/octet-stream, */*] Reader ReaderProvider (o.g.j.m.internal) [text/plain, */*] RenderedImage RenderedImageProvider (o.g.j.m.internal) [image/*] + EntityPart SingleEntityPartWriter (o.g.j.m.m.internal) Source SourceWriter (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] StreamingOutput StreamingOutputProvider (o.g.j.m.internal) [application/octet-stream, */*] String StringMessageProvider (o.g.j.m.internal) [text/plain, */*] @@ -161,6 +170,7 @@ class CompleteRenderTest extends Specification { CookieParamValueParamProvider (o.g.j.s.i.inject) DelegatedInjectionValueParamProvider (o.g.j.s.i.inject) EntityParamValueParamProvider (o.g.j.s.i.inject) + FormDataParamValueParamProvider (o.g.j.m.m.internal) FormParamValueParamProvider (o.g.j.s.i.inject) GuiceValueParamProvider (r.v.d.g.c.h.support) HKValueParamProvider (r.v.d.g.c.h.s.hk) *jersey managed @@ -184,6 +194,7 @@ class CompleteRenderTest extends Specification { @QueryParam ParamInjectionResolver (o.g.j.s.i.inject) using QueryParamValueParamProvider @Uri ParamInjectionResolver (o.g.j.s.i.inject) using WebTargetValueParamProvider @BeanParam ParamInjectionResolver (o.g.j.s.i.inject) using BeanParamValueParamProvider + @FormDataParam ParamInjectionResolver (o.g.j.s.i.inject) using FormDataParamValueParamProvider """ as String; } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy index fef5936e7..defce43fd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ConfigOptionsRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -38,7 +38,8 @@ class ConfigOptionsRenderTest extends Specification { render(new JerseyConfig().showExceptionMappers()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IOException GuiceExceptionMapper (r.v.d.g.c.h.support) @@ -87,6 +88,7 @@ class ConfigOptionsRenderTest extends Specification { byte[] ByteArrayProvider (o.g.j.m.internal) [application/octet-stream, */*] DataSource DataSourceProvider (o.g.j.m.internal) [application/octet-stream, */*] Document DocumentProvider (o.g.j.jaxb.internal) [application/xml, text/xml, */*] + List EntityPartReader (o.g.j.m.m.internal) [multipart/form-data] Enum EnumMessageProvider (o.g.j.m.internal) [text/plain] File FileProvider (o.g.j.m.internal) [application/octet-stream, */*] MultivaluedMap FormMultivaluedMapProvider (o.g.j.m.internal) [application/x-www-form-urlencoded] @@ -94,10 +96,13 @@ class ConfigOptionsRenderTest extends Specification { Type GuiceMessageBodyReader (r.v.d.g.c.h.support) Type HKMessageBodyReader (r.v.d.g.c.h.s.hk) *jersey managed InputStream InputStreamProvider (o.g.j.m.internal) [application/octet-stream, */*] - Object JacksonJsonProvider (c.f.j.jaxrs.json) [*/*] + Object JacksonJsonProvider (c.f.j.j.rs.json) [*/*] Object JacksonMessageBodyProvider (i.d.jersey.jackson) [*/*] + MultiPart MultiPartReaderServerSide (o.g.j.m.m.internal) [multipart/*] + Path PathProvider (o.g.j.m.internal) [application/octet-stream, */*] Reader ReaderProvider (o.g.j.m.internal) [text/plain, */*] RenderedImage RenderedImageProvider (o.g.j.m.internal) [image/*, application/octet-stream] + EntityPart SingleEntityPartReader (o.g.j.m.m.internal) DOMSource DomSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] SAXSource SaxSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] StreamSource StreamSourceReader (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] @@ -128,6 +133,7 @@ class ConfigOptionsRenderTest extends Specification { ChunkedOutput ChunkedResponseWriter (o.g.jersey.server) DataSource DataSourceProvider (o.g.j.m.internal) [application/octet-stream, */*] Document DocumentProvider (o.g.j.jaxb.internal) [application/xml, text/xml, */*] + List EntityPartWriter (o.g.j.m.m.internal) [multipart/form-data] Enum EnumMessageProvider (o.g.j.m.internal) [text/plain] File FileProvider (o.g.j.m.internal) [application/octet-stream, */*] MultivaluedMap FormMultivaluedMapProvider (o.g.j.m.internal) [application/x-www-form-urlencoded] @@ -135,15 +141,18 @@ class ConfigOptionsRenderTest extends Specification { Type GuiceMessageBodyWriter (r.v.d.g.c.h.support) Type HKMessageBodyWriter (r.v.d.g.c.h.s.hk) *jersey managed InputStream InputStreamProvider (o.g.j.m.internal) [application/octet-stream, */*] - Object JacksonJsonProvider (c.f.j.jaxrs.json) [*/*] - Object JacksonMessageBodyProvider (i.d.jersey.jackson) [*/*] + Object JacksonJsonProvider (c.f.j.j.rs.json) [application/json, text/json, */*] + Object JacksonMessageBodyProvider (i.d.jersey.jackson) [application/json, text/json, */*] + MultiPart MultiPartWriter (o.g.j.m.m.internal) [multipart/*] OptionalDouble OptionalDoubleMessageBodyWriter (i.d.jersey.optional) [*/*] OptionalInt OptionalIntMessageBodyWriter (i.d.jersey.optional) [*/*] OptionalLong OptionalLongMessageBodyWriter (i.d.jersey.optional) [*/*] Optional OptionalMessageBodyWriter (i.d.jersey.guava) [*/*] Optional OptionalMessageBodyWriter (i.d.jersey.optional) [*/*] + Path PathProvider (o.g.j.m.internal) [application/octet-stream, */*] Reader ReaderProvider (o.g.j.m.internal) [text/plain, */*] RenderedImage RenderedImageProvider (o.g.j.m.internal) [image/*] + EntityPart SingleEntityPartWriter (o.g.j.m.m.internal) Source SourceWriter (o.g.j.m.i.SourceProvider) [application/xml, text/xml, */*] StreamingOutput StreamingOutputProvider (o.g.j.m.internal) [application/octet-stream, */*] String StringMessageProvider (o.g.j.m.internal) [text/plain, */*] @@ -231,6 +240,7 @@ class ConfigOptionsRenderTest extends Specification { CookieParamValueParamProvider (o.g.j.s.i.inject) DelegatedInjectionValueParamProvider (o.g.j.s.i.inject) EntityParamValueParamProvider (o.g.j.s.i.inject) + FormDataParamValueParamProvider (o.g.j.m.m.internal) FormParamValueParamProvider (o.g.j.s.i.inject) GuiceValueParamProvider (r.v.d.g.c.h.support) HKValueParamProvider (r.v.d.g.c.h.s.hk) *jersey managed @@ -261,6 +271,7 @@ class ConfigOptionsRenderTest extends Specification { @QueryParam ParamInjectionResolver (o.g.j.s.i.inject) using QueryParamValueParamProvider @Uri ParamInjectionResolver (o.g.j.s.i.inject) using WebTargetValueParamProvider @BeanParam ParamInjectionResolver (o.g.j.s.i.inject) using BeanParamValueParamProvider + @FormDataParam ParamInjectionResolver (o.g.j.s.i.inject) using FormDataParamValueParamProvider """ as String; } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy index d84197e0a..e91d2a8e3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/EmptySectionRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -38,7 +38,8 @@ class EmptySectionRenderTest extends Specification { render(new JerseyConfig().showExceptionMappers().showContextResolvers()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IllegalStateException IllegalStateExceptionMapper (i.d.jersey.errors) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy index bef525c1b..cffcf965a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/ExtendedExceptionMapperRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import org.glassfish.jersey.spi.ExtendedExceptionMapper import ru.vyarus.dropwizard.guice.GuiceBundle @@ -13,9 +13,9 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -42,7 +42,8 @@ class ExtendedExceptionMapperRenderTest extends Specification { render(new JerseyConfig().showExceptionMappers()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IOException ExtMapper (r.v.d.g.d.r.j.ExtendedExceptionMapperRenderTest) *extended diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy index 7db96bec0..f4e7cee44 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterAnn.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import javax.ws.rs.NameBinding +import jakarta.ws.rs.NameBinding import java.lang.annotation.ElementType import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy index 28c31131b..1b2a47eae 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/FilterMarkerRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -12,10 +12,10 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -41,7 +41,8 @@ class FilterMarkerRenderTest extends Specification { render(new JerseyConfig().showExceptionMappers()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IOException ExMapper (r.v.d.g.d.r.j.FilterMarkerRenderTest) *only @FilterAnn diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy index 96263adbd..a802622c9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/LazyRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -13,10 +13,10 @@ import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -42,7 +42,8 @@ class LazyRenderTest extends Specification { render(new JerseyConfig().showExceptionMappers()) == """ Exception mappers - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IllegalStateException IllegalStateExceptionMapper (i.d.jersey.errors) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy index 6b7a71633..c2443a558 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/jersey/PriorityCountInRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.jersey -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -12,11 +12,11 @@ import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.annotation.Priority -import javax.inject.Inject -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.annotation.Priority +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -44,7 +44,8 @@ class PriorityCountInRenderTest extends Specification { Exception mappers IOException Mapper1 (r.v.d.g.d.r.j.PriorityCountInRenderTest) IOException Mapper2 (r.v.d.g.d.r.j.PriorityCountInRenderTest) - Throwable ExceptionMapperBinder\$1 (io.dropwizard.setup) + Throwable ExceptionMapperBinder\$1 (i.d.core.setup) + Throwable DefaultExceptionMapper (o.g.jersey.server) EofException EarlyEofExceptionMapper (i.d.jersey.errors) EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) IllegalStateException IllegalStateExceptionMapper (i.d.jersey.errors) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy index 15774fc43..8f8ca04d8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/ConfiguredRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.web.support.UserServletsBundle @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -53,6 +53,7 @@ class ConfiguredRenderTest extends Specification { ├── filter /2/* --"-- ├── filter /* async GuiceFilter (c.g.inject.servlet) [REQUEST] Guice Filter ├── filter /* async AllowedMethodsFilter (i.d.jersey.filter) [REQUEST] io.dropwizard.jersey.filter.AllowedMethodsFilter-11111111 + ├── filter /* async ZipExceptionHandlingServletFilter (io.dropwizard.jetty) [REQUEST] io.dropwizard.jetty.ZipExceptionHandlingServletFilter-11111111 ├── filter /* async ThreadNameFilter (i.d.servlets) [REQUEST] io.dropwizard.servlets.ThreadNameFilter-11111111 │ ├── servlet /foo MainServlet (r.v.d.g.d.r.w.s.UserServletsBundle) target @@ -62,7 +63,7 @@ class ConfiguredRenderTest extends Specification { ├── servlet /both BothServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .both ├── servlet /async async AsyncServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .async ├── servlet /* async JerseyServletContainer (i.d.jersey.setup) jersey - └── servlet / async Default404Servlet (o.e.j.s.ServletHandler) org.eclipse.jetty.servlet.ServletHandler\$Default404Servlet-11111111 + └── servlet / async Default404Servlet (o.e.j.e.s.ServletHandler) org.eclipse.jetty.ee10.servlet.ServletHandler\$Default404Servlet-11111111 ADMIN / @@ -75,8 +76,8 @@ class ConfiguredRenderTest extends Specification { ├── servlet /fooadmin AdminServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .admin ├── servlet /baradmin --"-- ├── servlet /both BothServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .both - ├── servlet /* async AdminServlet (c.c.metrics.servlets) com.codahale.metrics.servlets.AdminServlet-11111111 - └── servlet / async Default404Servlet (o.e.j.s.ServletHandler) org.eclipse.jetty.servlet.ServletHandler\$Default404Servlet-11111111 + ├── servlet /* async AdminServlet (i.d.metrics.servlets) io.dropwizard.metrics.servlets.AdminServlet-11111111 + └── servlet / async Default404Servlet (o.e.j.e.s.ServletHandler) org.eclipse.jetty.ee10.servlet.ServletHandler\$Default404Servlet-11111111 """ as String; } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy index 4a42a3d0c..8c20d96cb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/GuiceFilterRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.web.support.GuiceWebModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy index 1cd4705c0..6d80c20c9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/SimpleServerRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.report.web.MappingsConfig @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -47,16 +47,17 @@ class SimpleServerRenderTest extends Specification { MAIN /app ├── filter /* async AllowedMethodsFilter (i.d.jersey.filter) [REQUEST] io.dropwizard.jersey.filter.AllowedMethodsFilter-11111111 + ├── filter /* async ZipExceptionHandlingServletFilter (io.dropwizard.jetty) [REQUEST] io.dropwizard.jetty.ZipExceptionHandlingServletFilter-11111111 ├── filter /* async ThreadNameFilter (i.d.servlets) [REQUEST] io.dropwizard.servlets.ThreadNameFilter-11111111 ├── servlet /rest/* async JerseyServletContainer (i.d.jersey.setup) jersey - └── servlet / async Default404Servlet (o.e.j.s.ServletHandler) org.eclipse.jetty.servlet.ServletHandler\$Default404Servlet-11111111 + └── servlet / async Default404Servlet (o.e.j.e.s.ServletHandler) org.eclipse.jetty.ee10.servlet.ServletHandler\$Default404Servlet-11111111 ADMIN /admin ├── filter /* async AllowedMethodsFilter (i.d.jersey.filter) [REQUEST] io.dropwizard.jersey.filter.AllowedMethodsFilter-11111111 ├── servlet /tasks/* async TaskServlet (i.d.servlets.tasks) tasks - ├── servlet /* async AdminServlet (c.c.metrics.servlets) com.codahale.metrics.servlets.AdminServlet-11111111 - └── servlet / async Default404Servlet (o.e.j.s.ServletHandler) org.eclipse.jetty.servlet.ServletHandler\$Default404Servlet-11111111 + ├── servlet /* async AdminServlet (i.d.metrics.servlets) io.dropwizard.metrics.servlets.AdminServlet-11111111 + └── servlet / async Default404Servlet (o.e.j.e.s.ServletHandler) org.eclipse.jetty.ee10.servlet.ServletHandler\$Default404Servlet-11111111 """ as String; } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy index f395be300..e6dc587c5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/UserServletsRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.web.support.UserServletsBundle @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -103,7 +103,10 @@ class UserServletsRenderTest extends Specification { ├── servlet /fooadmin AdminServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .admin ├── servlet /baradmin --"-- └── servlet /both BothServlet (r.v.d.g.d.r.w.s.UserServletsBundle) .both -""" as String; +""" as String + + // because jetty performs servlets update on stop and disabled servlet would lead to error + servlet.setEnabled(true) } static class App extends Application { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy index ffd4ca8a0..5120257d3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/WebReportUnderLightweightGuiceyTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.debug.renderer.web.support.GuiceWebModule @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy index 13c27943e..1ef2396b4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/GuiceWebModule.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.debug.renderer.web.support import com.google.inject.servlet.ServletModule -import javax.inject.Singleton -import javax.servlet.http.HttpFilter -import javax.servlet.http.HttpServlet +import jakarta.inject.Singleton +import jakarta.servlet.http.HttpFilter +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy index 4ef4df4c6..9095ef034 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/renderer/web/support/UserServletsBundle.groovy @@ -4,11 +4,11 @@ import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.DispatcherType -import javax.servlet.annotation.WebFilter -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpFilter -import javax.servlet.http.HttpServlet +import jakarta.servlet.DispatcherType +import jakarta.servlet.annotation.WebFilter +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpFilter +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviatorTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviatorTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviatorTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/ClassNameAbbreviatorTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/RendererUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/RendererUtilsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/RendererUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/RendererUtilsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/TreeRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/TreeRenderTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/TreeRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/TreeRenderTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/support/WithAnonymous.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/support/WithAnonymous.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/support/WithAnonymous.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/util/support/WithAnonymous.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy index b379a8f3b..cd8ae970e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithBindingsPrintTest.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import ru.vyarus.dropwizard.guice.yaml.support.ComplexGenericCase diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy index 9587abefe..907bfc5b7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithCustomBindingsPrintTest.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import ru.vyarus.dropwizard.guice.yaml.support.ComplexGenericCase diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy index a1809c663..9b97ec914 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/AppWithEmptyCustomBindingsPrintTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy index add998e87..9ee58c319 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/BindingsReportTest.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import ru.vyarus.dropwizard.guice.yaml.support.ComplexGenericCase import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -30,7 +30,9 @@ class BindingsReportTest extends Specification { // @Config("server.requestLog") RequestLogFactory, but its not because // type is lowered on declaration: private RequestLogFactory requestLog; (AbstractServerFactory) // SO case is: type information INTENTIONALLY lowered - render(new BindingsConfig()) == """ + render(new BindingsConfig()) + .replace('[HEAD, DELETE, POST, GET, OPTIONS, PATCH, PUT]', '[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]') + == """ Configuration object bindings: @Config ComplexGenericCase @@ -49,7 +51,7 @@ class BindingsReportTest extends Specification { @Config TaskConfiguration = TaskConfiguration[printStackTraceOnError=false] Configuration.logging - @Config LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.ConsoleAppenderFactory@1111111]} + @Config LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.common.ConsoleAppenderFactory@1111111]} Configuration.metrics @Config MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} @@ -63,9 +65,6 @@ class BindingsReportTest extends Specification { Configuration.server.requestLog @Config RequestLogFactory (with actual type LogbackAccessRequestLogFactory) = io.dropwizard.request.logging.LogbackAccessRequestLogFactory@1111111 - Configuration.server.serverPush - @Config ServerPushFilterFactory = io.dropwizard.jetty.ServerPushFilterFactory@1111111 - Configuration paths bindings: @@ -79,8 +78,8 @@ class BindingsReportTest extends Specification { @Config("admin.tasks") TaskConfiguration = TaskConfiguration[printStackTraceOnError=false] @Config("admin.tasks.printStackTraceOnError") Boolean = false @Config("health") Optional = Optional.empty - @Config("logging") LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.ConsoleAppenderFactory@1111111]} - @Config("logging.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + @Config("logging") LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.common.ConsoleAppenderFactory@1111111]} + @Config("logging.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] @Config("logging.level") String = "INFO" @Config("logging.loggers") Map (with actual type HashMap) = {} @Config("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} @@ -98,27 +97,23 @@ class BindingsReportTest extends Specification { @Config("server.detailedJsonProcessingExceptionMapper") Boolean = false @Config("server.dumpAfterStart") Boolean = false @Config("server.dumpBeforeStop") Boolean = false + @Config("server.enableAdminVirtualThreads") Boolean = false @Config("server.enableThreadNameFilter") Boolean = true + @Config("server.enableVirtualThreads") Boolean = false @Config("server.gzip") GzipHandlerFactory = io.dropwizard.jetty.GzipHandlerFactory@1111111 @Config("server.gzip.bufferSize") DataSize = 8 kibibytes @Config("server.gzip.deflateCompressionLevel") Integer = -1 @Config("server.gzip.enabled") Boolean = true - @Config("server.gzip.excludedUserAgentPatterns") Set (with actual type HashSet) = [] - @Config("server.gzip.gzipCompatibleInflation") Boolean = true @Config("server.gzip.minimumEntitySize") DataSize = 256 bytes @Config("server.gzip.syncFlush") Boolean = false @Config("server.idleThreadTimeout") Duration = 1 minute - @Config("server.maxQueuedRequests") Integer = 1024 @Config("server.maxThreads") Integer = 1024 @Config("server.minThreads") Integer = 8 @Config("server.registerDefaultExceptionMappers") Boolean = true @Config("server.requestLog") RequestLogFactory (with actual type LogbackAccessRequestLogFactory) = io.dropwizard.request.logging.LogbackAccessRequestLogFactory@1111111 - @Config("server.requestLog.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + @Config("server.requestLog.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] + @Config("server.responseMeteredLevel") ResponseMeteredLevel = COARSE @Config("server.rootPath") Optional = Optional.empty - @Config("server.serverPush") ServerPushFilterFactory = io.dropwizard.jetty.ServerPushFilterFactory@1111111 - @Config("server.serverPush.associatePeriod") Duration = 4 seconds - @Config("server.serverPush.enabled") Boolean = false - @Config("server.serverPush.maxAssociations") Integer = 16 @Config("server.shutdownGracePeriod") Duration = 30 seconds """ } @@ -132,7 +127,9 @@ class BindingsReportTest extends Specification { // SO case is: type information INTENTIONALLY lowered render(new BindingsConfig() .showConfigurationTree() - .showNullValues()) == """ + .showNullValues()) + .replace('[HEAD, DELETE, POST, GET, OPTIONS, PATCH, PUT]', '[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]') + == """ ComplexGenericCase (visible paths) │ @@ -153,7 +150,7 @@ class BindingsReportTest extends Specification { ├── health: Optional = Optional.empty │ ├── logging: DefaultLoggingFactory - │ ├── appenders: ArrayList> = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + │ ├── appenders: ArrayList> = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] │ ├── level: String = "INFO" │ └── loggers: HashMap = {} │ @@ -173,16 +170,19 @@ class BindingsReportTest extends Specification { ├── detailedJsonProcessingExceptionMapper: Boolean = false ├── dumpAfterStart: Boolean = false ├── dumpBeforeStop: Boolean = false + ├── enableAdminVirtualThreads: Boolean = false ├── enableThreadNameFilter: Boolean = true + ├── enableVirtualThreads: Boolean = false ├── gid: Integer = null ├── group: String = null ├── idleThreadTimeout: Duration = 1 minute - ├── maxQueuedRequests: Integer = 1024 ├── maxThreads: Integer = 1024 + ├── metricPrefix: String = null ├── minThreads: Integer = 8 ├── nofileHardLimit: Integer = null ├── nofileSoftLimit: Integer = null ├── registerDefaultExceptionMappers: Boolean = true + ├── responseMeteredLevel: ResponseMeteredLevel = COARSE ├── rootPath: Optional = Optional.empty ├── shutdownGracePeriod: Duration = 30 seconds ├── startsAsRoot: Boolean = null @@ -197,22 +197,13 @@ class BindingsReportTest extends Specification { │ ├── enabled: Boolean = true │ ├── excludedMimeTypes: Set = null │ ├── excludedPaths: Set = null - │ ├── excludedUserAgentPatterns: HashSet = [] - │ ├── gzipCompatibleInflation: Boolean = true │ ├── includedMethods: Set = null │ ├── includedPaths: Set = null │ ├── minimumEntitySize: DataSize = 256 bytes │ └── syncFlush: Boolean = false │ - ├── requestLog: LogbackAccessRequestLogFactory - │ └── appenders: ArrayList> = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] - │ - └── serverPush: ServerPushFilterFactory - ├── associatePeriod: Duration = 4 seconds - ├── enabled: Boolean = false - ├── maxAssociations: Integer = 16 - ├── refererHosts: List = null - └── refererPorts: List = null + └── requestLog: LogbackAccessRequestLogFactory + └── appenders: ArrayList> = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] Configuration object bindings: @@ -235,7 +226,7 @@ class BindingsReportTest extends Specification { @Config TaskConfiguration = TaskConfiguration[printStackTraceOnError=false] Configuration.logging - @Config LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.ConsoleAppenderFactory@1111111]} + @Config LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.common.ConsoleAppenderFactory@1111111]} Configuration.metrics @Config MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} @@ -249,9 +240,6 @@ class BindingsReportTest extends Specification { Configuration.server.requestLog @Config RequestLogFactory (with actual type LogbackAccessRequestLogFactory) = io.dropwizard.request.logging.LogbackAccessRequestLogFactory@1111111 - Configuration.server.serverPush - @Config ServerPushFilterFactory = io.dropwizard.jetty.ServerPushFilterFactory@1111111 - Configuration paths bindings: @@ -269,8 +257,8 @@ class BindingsReportTest extends Specification { @Config("admin.tasks") TaskConfiguration = TaskConfiguration[printStackTraceOnError=false] @Config("admin.tasks.printStackTraceOnError") Boolean = false @Config("health") Optional = Optional.empty - @Config("logging") LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.ConsoleAppenderFactory@1111111]} - @Config("logging.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + @Config("logging") LoggingFactory (with actual type DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.common.ConsoleAppenderFactory@1111111]} + @Config("logging.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] @Config("logging.level") String = "INFO" @Config("logging.loggers") Map (with actual type HashMap) = {} @Config("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} @@ -288,7 +276,9 @@ class BindingsReportTest extends Specification { @Config("server.detailedJsonProcessingExceptionMapper") Boolean = false @Config("server.dumpAfterStart") Boolean = false @Config("server.dumpBeforeStop") Boolean = false + @Config("server.enableAdminVirtualThreads") Boolean = false @Config("server.enableThreadNameFilter") Boolean = true + @Config("server.enableVirtualThreads") Boolean = false @Config("server.gid") Integer = null @Config("server.group") String = null @Config("server.gzip") GzipHandlerFactory = io.dropwizard.jetty.GzipHandlerFactory@1111111 @@ -298,28 +288,21 @@ class BindingsReportTest extends Specification { @Config("server.gzip.enabled") Boolean = true @Config("server.gzip.excludedMimeTypes") Set = null @Config("server.gzip.excludedPaths") Set = null - @Config("server.gzip.excludedUserAgentPatterns") Set (with actual type HashSet) = [] - @Config("server.gzip.gzipCompatibleInflation") Boolean = true @Config("server.gzip.includedMethods") Set = null @Config("server.gzip.includedPaths") Set = null @Config("server.gzip.minimumEntitySize") DataSize = 256 bytes @Config("server.gzip.syncFlush") Boolean = false @Config("server.idleThreadTimeout") Duration = 1 minute - @Config("server.maxQueuedRequests") Integer = 1024 @Config("server.maxThreads") Integer = 1024 + @Config("server.metricPrefix") String = null @Config("server.minThreads") Integer = 8 @Config("server.nofileHardLimit") Integer = null @Config("server.nofileSoftLimit") Integer = null @Config("server.registerDefaultExceptionMappers") Boolean = true @Config("server.requestLog") RequestLogFactory (with actual type LogbackAccessRequestLogFactory) = io.dropwizard.request.logging.LogbackAccessRequestLogFactory@1111111 - @Config("server.requestLog.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + @Config("server.requestLog.appenders") List> (with actual type ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] + @Config("server.responseMeteredLevel") ResponseMeteredLevel = COARSE @Config("server.rootPath") Optional = Optional.empty - @Config("server.serverPush") ServerPushFilterFactory = io.dropwizard.jetty.ServerPushFilterFactory@1111111 - @Config("server.serverPush.associatePeriod") Duration = 4 seconds - @Config("server.serverPush.enabled") Boolean = false - @Config("server.serverPush.maxAssociations") Integer = 16 - @Config("server.serverPush.refererHosts") List = null - @Config("server.serverPush.refererPorts") List = null @Config("server.shutdownGracePeriod") Duration = 30 seconds @Config("server.startsAsRoot") Boolean = null @Config("server.uid") Integer = null @@ -333,7 +316,9 @@ class BindingsReportTest extends Specification { render(new BindingsConfig() .showCustomConfigOnly() .showNullValues() - .showConfigurationTree()) == """ + .showConfigurationTree()) + .replace('[HEAD, DELETE, POST, GET, OPTIONS, PATCH, PUT]', '[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]') + == """ ComplexGenericCase (visible paths) │ @@ -375,7 +360,9 @@ class BindingsReportTest extends Specification { def "Check tree only report"() { expect: render(new BindingsConfig() - .showConfigurationTreeOnly()) == """ + .showConfigurationTreeOnly()) + .replace('[HEAD, DELETE, POST, GET, OPTIONS, PATCH, PUT]', '[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]') + == """ ComplexGenericCase (visible paths) │ @@ -393,7 +380,7 @@ class BindingsReportTest extends Specification { ├── health: Optional = Optional.empty │ ├── logging: DefaultLoggingFactory - │ ├── appenders: ArrayList> = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] + │ ├── appenders: ArrayList> = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] │ ├── level: String = "INFO" │ └── loggers: HashMap = {} │ @@ -413,12 +400,14 @@ class BindingsReportTest extends Specification { ├── detailedJsonProcessingExceptionMapper: Boolean = false ├── dumpAfterStart: Boolean = false ├── dumpBeforeStop: Boolean = false + ├── enableAdminVirtualThreads: Boolean = false ├── enableThreadNameFilter: Boolean = true + ├── enableVirtualThreads: Boolean = false ├── idleThreadTimeout: Duration = 1 minute - ├── maxQueuedRequests: Integer = 1024 ├── maxThreads: Integer = 1024 ├── minThreads: Integer = 8 ├── registerDefaultExceptionMappers: Boolean = true + ├── responseMeteredLevel: ResponseMeteredLevel = COARSE ├── rootPath: Optional = Optional.empty ├── shutdownGracePeriod: Duration = 30 seconds │ @@ -426,18 +415,11 @@ class BindingsReportTest extends Specification { │ ├── bufferSize: DataSize = 8 kibibytes │ ├── deflateCompressionLevel: Integer = -1 │ ├── enabled: Boolean = true - │ ├── excludedUserAgentPatterns: HashSet = [] - │ ├── gzipCompatibleInflation: Boolean = true │ ├── minimumEntitySize: DataSize = 256 bytes │ └── syncFlush: Boolean = false │ - ├── requestLog: LogbackAccessRequestLogFactory - │ └── appenders: ArrayList> = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] - │ - └── serverPush: ServerPushFilterFactory - ├── associatePeriod: Duration = 4 seconds - ├── enabled: Boolean = false - └── maxAssociations: Integer = 16 + └── requestLog: LogbackAccessRequestLogFactory + └── appenders: ArrayList> = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] """ } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy index 85fc2d71b..40713a86e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/EmptyReportRenderTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy index dd1677579..cbde8056e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/debug/yaml/StringValueEscapeReportTest.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.debug.yaml -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import ru.vyarus.dropwizard.guice.yaml.support.SimpleConfig import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy index 386acf9dc..2a5861a03 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.diagnostic.support.AutoScanApp import ru.vyarus.dropwizard.guice.diagnostic.support.features.* import ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule @@ -25,7 +25,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.web.listener.WebListe import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy index 4c4537018..3333354a2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithBundleDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.diagnostic.support.AutoScanAppWithBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.* import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller @@ -28,7 +28,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.web.listener.WebListe import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy index 0bb3b7ea7..7f507dfb0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/AutoScanModeWithLookupDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup import ru.vyarus.dropwizard.guice.diagnostic.support.AutoScanAppWithLookup import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.* @@ -32,7 +32,7 @@ import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/BaseDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/BaseDiagnosticTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/BaseDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/BaseDiagnosticTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy index 95ca20ac0..ff2237254 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/CommandInfoItemTest.groovy @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy index 4f35bceec..08768da27 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ConfigInfoItemsTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.diagnostic.support.ManualAppWithBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.FooBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.FooBundleResource @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstal import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy index c38b07bc8..0fbff2c21 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ExtensionInfoCasesTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo @@ -13,8 +13,8 @@ import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject -import javax.ws.rs.Path +import jakarta.inject.Inject +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy index b8bc1ca9b..333f9fa90 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.diagnostic.support.ManualApp import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.context.info.ItemId import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy index 63222cd0b..ff2806d8e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithBundleDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.diagnostic.support.ManualAppWithBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.* import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy index 721331df4..eb36047f2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/ManualModeWithLookupDiagnosticTest.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup import ru.vyarus.dropwizard.guice.diagnostic.support.ManualAppWithLookup import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller @@ -18,7 +18,7 @@ import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject import static ru.vyarus.dropwizard.guice.module.context.info.ItemId.typesOnly diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy index b9ca3a3c6..09080e225 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanApp.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooResource diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy index bd9380d93..4cdb0d0b9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithBundle.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.FooBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy index f6003d748..a7dc957d8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/AutoScanAppWithLookup.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.FooBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy index 153c91b0c..968389745 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualApp.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy index bf62f1735..0db2e28ad 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithBundle.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.bundle.FooBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy index a5b01314f..8b5d7ca7f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/ManualAppWithLookup.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooInstaller import ru.vyarus.dropwizard.guice.diagnostic.support.features.FooModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/Foo2Bundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/Foo2Bundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/Foo2Bundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/Foo2Bundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleInstaller.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleInstaller.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelative2Bundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelative2Bundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelative2Bundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelative2Bundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelativeBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelativeBundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelativeBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleRelativeBundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy index 85b1b3a07..04732d7cd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/FooBundleResource.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.bundle -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/LookupBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/LookupBundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/LookupBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/bundle/LookupBundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy index 071e37d71..156e7df37 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/dwbundle/FooDwBundle.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.dwbundle -import io.dropwizard.ConfiguredBundle +import io.dropwizard.core.ConfiguredBundle /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy index 3e3b1ac75..ccff90020 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/Cli.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.features -import io.dropwizard.cli.Command -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.cli.Command +import io.dropwizard.core.setup.Bootstrap import net.sourceforge.argparse4j.inf.Namespace import net.sourceforge.argparse4j.inf.Subparser diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy similarity index 73% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy index 5e2510948..f02cdbc83 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/EnvCommand.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.features -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.cli.EnvironmentCommand -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.cli.EnvironmentCommand +import io.dropwizard.core.setup.Environment import net.sourceforge.argparse4j.inf.Namespace /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooInstaller.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooInstaller.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy index 0a6ff0f19..815bce17f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/features/FooResource.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.features -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy index ac8c5aaa3..6af091ed6 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/ModuleWithExtensions.groovy @@ -2,7 +2,7 @@ package ru.vyarus.dropwizard.guice.diagnostic.support.module import com.google.inject.AbstractModule -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/OverridingModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/OverridingModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/OverridingModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/diagnostic/support/module/OverridingModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy index 9deba6239..b461fbb95 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingDisableTest.groovy @@ -1,16 +1,16 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.* -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy index 6eed7aa1f..6169ec1b5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/BindingRegistrationTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy index 11766d684..fd2bb7400 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/DisableInnerGuiceModuleTest.groovy @@ -4,10 +4,10 @@ import com.google.inject.AbstractModule import com.google.inject.Inject import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy index d3840b7f0..5922d8a8d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingWithAutoScanTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.* -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -48,7 +48,7 @@ class ExtensionBindingWithAutoScanTest extends AbstractTest { info.getExtensions(LifeCycleInstaller) == [DummyLifeCycle] then: "jersey provider found" - info.getExtensions(JerseyProviderInstaller) as Set == [DummyExceptionMapper, DummyJerseyProvider, DummyOtherProvider] as Set + info.getExtensions(JerseyProviderInstaller) as Set == [DummyExceptionMapper, DummyJerseyProvider, DummyModelProcessor, DummyOtherProvider] as Set then: "feature found" info.getExtensions(JerseyFeatureInstaller) as Set == [DummyFeature, HK2DebugFeature] as Set diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy index a53cb94ae..8594803a6 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/ExtensionBindingsTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.* -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy index 3efcf46fe..a5d406c6e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedInstanceDetectionTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.context.ConfigItem import ru.vyarus.dropwizard.guice.module.context.info.ItemId import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy index f4213b9d3..716c3cba4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyDetectionTest.groovy @@ -2,17 +2,17 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy index fbe31ca19..7de827d9e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyRemoveTest.groovy @@ -2,17 +2,17 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy index aaaae2094..c7368c630 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/LinkedKeyResolutionOrderTest.groovy @@ -2,17 +2,17 @@ package ru.vyarus.dropwizard.guice.guiceconfig import com.google.inject.AbstractModule import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BindingFromPrivateModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BindingFromPrivateModuleTest.groovy new file mode 100644 index 000000000..047be226e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BindingFromPrivateModuleTest.groovy @@ -0,0 +1,83 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 18.10.2024 + */ +@TestGuiceyApp(App) +class BindingFromPrivateModuleTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"() { + + expect: "module detected" + info.getModules().contains(Module) + injector.getInstance(Ext) + + and: "extension recognized" + ExtensionItemInfo ext = info.getInfo(Ext) + ext != null + ext.isGuiceBinding() + ext.getRegistrationScope().getType() == Module + ext.getRegistrationScopeType() == ConfigScope.Module + ext.getRegistrationAttempts() == 1 + + and: "inner extension not recognized" + info.getInfo(InnerExt) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + // visible extension + bind(Bind).to(Ext) + expose(Bind) + + // not visible extension + bind(InnerExt) + } + } + + static interface Bind {} + + static class Ext implements Bind, Managed { + } + + static class InnerExt implements Managed {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BoundNotExposedPrivateExtensionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BoundNotExposedPrivateExtensionTest.groovy new file mode 100644 index 000000000..ed8ae8a85 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/BoundNotExposedPrivateExtensionTest.groovy @@ -0,0 +1,86 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import jakarta.inject.Provider +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 31.01.2025 + */ +@TestGuiceyApp(App) +class BoundNotExposedPrivateExtensionTest extends AbstractTest { + + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"() { + + expect: "module detected" + info.getModules().contains(Module) + injector.getInstance(Ext) + + and: "extension recognized" + ExtensionItemInfo ext = info.getInfo(Ext) + ext != null + ext.isGuiceBinding() + ext.getRegistrationScope().getType() == Module + ext.getRegistrationScopeType() == ConfigScope.Module + ext.getRegistrationAttempts() == 1 + + and: "inner extension not recognized" + info.getInfo(InnerExt) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + // visible extension + bind(Bind).to(Ext) + // important: Ext not exposed, but binding already exists (no synthetic must be added) + bind(Ext).toProvider({ new Ext() } as Provider) + expose(Bind) + + // not visible extension + bind(InnerExt) + } + } + + static interface Bind {} + + static class Ext implements Bind, Managed { + } + + static class InnerExt implements Managed {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisablePrivateExtensionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisablePrivateExtensionTest.groovy new file mode 100644 index 000000000..4939ccb13 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisablePrivateExtensionTest.groovy @@ -0,0 +1,76 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 14.11.2024 + */ +@TestGuiceyApp(App) +class DisablePrivateExtensionTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check private binding remove for disabled extension"() { + + expect: "extension ignored recognized" + info.getInfo(Ext) != null + info.getInfo(Ext2) != null + + and: "binding removed" + injector.getExistingBinding(Key.get(Bind)) == null + injector.getExistingBinding(Key.get(Ext2)) != null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .disableExtensions(Ext) + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + // visible extension + bind(Bind).to(Ext) + expose(Bind) + + // ext must remain + bind(Ext2) + expose(Ext2) + } + } + + static interface Bind {} + + static class Ext implements Bind, Managed { + } + + static class Ext2 implements Managed { + } +} \ No newline at end of file diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateModuleTest.groovy new file mode 100644 index 000000000..14853da3a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateModuleTest.groovy @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.Key +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 14.11.2024 + */ +@TestGuiceyApp(App) +class DisabledPrivateModuleTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check private binding remove for disabled extension"() { + + expect: "extension ignored recognized" + info.getInfo(Ext) == null + + and: "module ignored" + !info.getModules().contains(Module) + info.getModulesDisabled().contains(Module) + + and: "binding removed" + injector.getExistingBinding(Key.get(Bind)) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .disableModules(Module) + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + // visible extension + bind(Bind).to(Ext) + expose(Bind) + } + } + + static interface Bind {} + + static class Ext implements Bind, Managed { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateSubModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateSubModuleTest.groovy new file mode 100644 index 000000000..bfc8d29be --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/DisabledPrivateSubModuleTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.AbstractModule +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 02.02.2025 + */ +@TestGuiceyApp(App) +class DisabledPrivateSubModuleTest extends AbstractTest { + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check private binding remove for disabled extension"() { + + expect: "extension ignored" + info.getInfo(Ext) == null + + and: "module ignored" + info.getModules().contains(Module) + info.getModulesDisabled().contains(SubModule) + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printAllGuiceBindings() + .modules(new Module()) + .disableModules(SubModule) + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + install(new SubModule()) + } + } + + static class SubModule extends AbstractModule { + @Override + protected void configure() { + bind(Bind).to(Ext) + } + } + + static interface Bind {} + + static class Ext implements Bind, Managed { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/ExposedBindingDeclaredInSubModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/ExposedBindingDeclaredInSubModuleTest.groovy new file mode 100644 index 000000000..598e38ad7 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/ExposedBindingDeclaredInSubModuleTest.groovy @@ -0,0 +1,83 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.AbstractModule +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 02.02.2025 + */ +@TestGuiceyApp(App) +class ExposedBindingDeclaredInSubModuleTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"() { + + expect: "module detected" + info.getModules().contains(Module) + injector.getInstance(Ext) + injector.getInstance(Ext2) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + install(new SubModule()) + install(new PrivateSubModule()) + expose(Ext) + expose(Ext2) + } + } + + private static class SubModule extends AbstractModule { + @Override + protected void configure() { + bind(Ext) + } + } + + private static class PrivateSubModule extends PrivateModule { + @Override + protected void configure() { + bind(Ext2) + expose(Ext2) + } + } + + static class Ext implements Managed { + } + + static class Ext2 implements Managed { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/IndirectWebExtensionsFromPrivateModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/IndirectWebExtensionsFromPrivateModuleTest.groovy new file mode 100644 index 000000000..5c6b588b4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/IndirectWebExtensionsFromPrivateModuleTest.groovy @@ -0,0 +1,94 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import com.google.inject.Singleton +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2025 + */ +@TestDropwizardApp(App) +class IndirectWebExtensionsFromPrivateModuleTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"(ClientSupport client) { + + expect: "managed installed" + info.getInfo(PrivateManaged) != null + injector.getInstance(PrivateManaged).called + + and: "resource recognized" + info.getInfo(PrivateResource) != null + client.targetRest('/private').request().get().readEntity(String) == "{\"done=\": true}" + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + bind(Managed).to(PrivateManaged) + expose(Managed) + + bind(PubRes).to(PrivateResource) + expose(PubRes) + } + } + + @Singleton + static class PrivateManaged implements Managed { + private boolean called + + @Override + void start() throws Exception { + called = true + } + } + + static interface PubRes {} + + @Path("/private") + @Produces('application/json') + static class PrivateResource implements PubRes { + @GET + @Path("/") + Response latest() { + return Response.ok("{\"done=\": true}").build(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/PrivateModulesAnalysisDisabledTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/PrivateModulesAnalysisDisabledTest.groovy new file mode 100644 index 000000000..994e76357 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/PrivateModulesAnalysisDisabledTest.groovy @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + + +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.GuiceyOptions +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 02.02.2025 + */ +@TestGuiceyApp(App) +class PrivateModulesAnalysisDisabledTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"() { + + expect: "extension not detected" + info.getModules().contains(Module) + !info.getInfo(Ext) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .option(GuiceyOptions.AnalyzePrivateGuiceModules, false) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + bind(Ext) + expose(Ext) + } + } + + static class Ext implements Managed { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/SimpleBindingFromPrivateModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/SimpleBindingFromPrivateModuleTest.groovy new file mode 100644 index 000000000..a47f3ad9b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/guiceconfig/prvt/SimpleBindingFromPrivateModuleTest.groovy @@ -0,0 +1,101 @@ +package ru.vyarus.dropwizard.guice.guiceconfig.prvt + +import com.google.inject.Exposed +import com.google.inject.Inject +import com.google.inject.Injector +import com.google.inject.PrivateModule +import com.google.inject.Provides +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.lifecycle.Managed +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2025 + */ +@TestGuiceyApp(App) +class SimpleBindingFromPrivateModuleTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check extensions registration form private binding"() { + + expect: "module detected" + info.getModules().contains(Module) + injector.getInstance(Ext) + injector.getInstance(Ext2) + + and: "extension recognized" + ExtensionItemInfo ext = info.getInfo(Ext) + ext != null + ext.isGuiceBinding() + ext.getRegistrationScope().getType() == Module + ext.getRegistrationScopeType() == ConfigScope.Module + ext.getRegistrationAttempts() == 1 + + and: "extension from provider recognized" + ExtensionItemInfo ext2 = info.getInfo(Ext2) + ext2 != null + ext2.isGuiceBinding() + ext2.getRegistrationScope().getType() == Module + ext2.getRegistrationScopeType() == ConfigScope.Module + ext2.getRegistrationAttempts() == 1 + + and: "inner extension not recognized" + info.getInfo(Bind) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new Module()) + .printAllGuiceBindings() + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Module extends PrivateModule { + @Override + protected void configure() { + // visible extension + bind(Ext) + expose(Ext) + + // not visible extension + bind(Bind).to(InnerExt) + } + + @Provides @Exposed + Ext2 provide() { + new Ext2() + } + } + + static interface Bind {} + + static class Ext implements Managed { + } + + static class Ext2 implements Managed { + } + + static class InnerExt implements Bind, Managed {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy index 7e80cffe8..672df4ad0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/DuplicateListenersIgnoredTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.lifecycle -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy index e0fdec761..f5e574dcd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/EventsConsistencyTest.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.lifecycle import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup @@ -36,13 +36,13 @@ import ru.vyarus.dropwizard.guice.support.feature.DummyTask import ru.vyarus.dropwizard.guice.support.util.BindModule import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov * @since 23.04.2018 */ -@TestDropwizardApp(value = App, hooks = XConf) +@TestDropwizardApp(value = App, hooks = XConf, useDefaultExtensions = false) class EventsConsistencyTest extends AbstractTest { def "Check events consistency"() { @@ -143,6 +143,11 @@ class EventsConsistencyTest extends AbstractTest { assert event.hooks[1] instanceof XConf } + @Override + protected void beforeInit(BeforeInitEvent event) { + confChecks(event) + } + @Override protected void dropwizardBundlesInitialized(DropwizardBundlesInitializedEvent event) { confChecks(event) @@ -191,17 +196,10 @@ class EventsConsistencyTest extends AbstractTest { assert event.disabled.size() == 1 } - @Override - protected void manualExtensionsValidated(ManualExtensionsValidatedEvent event) { - confChecks(event) - assert event.extensions.size() == 1 - assert event.validated.size() == 0 - } - @Override protected void classpathExtensionsResolved(ClasspathExtensionsResolvedEvent event) { confChecks(event) - assert event.extensions.size() == 14 + assert event.extensions.size() == 15 } @Override @@ -221,6 +219,13 @@ class EventsConsistencyTest extends AbstractTest { assert event.getBundles().size() == 5 } + @Override + protected void manualExtensionsValidated(ManualExtensionsValidatedEvent event) { + confChecks(event) + assert event.extensions.size() == 1 + assert event.validated.size() == 0 + } + @Override protected void modulesAnalyzed(ModulesAnalyzedEvent event) { runChecks(event) @@ -233,7 +238,7 @@ class EventsConsistencyTest extends AbstractTest { @Override protected void extensionsResolved(ExtensionsResolvedEvent event) { runChecks(event) - assert event.extensions.size() == 13 + assert event.extensions.size() == 14 assert event.disabled.size() == 3 } @@ -282,17 +287,24 @@ class EventsConsistencyTest extends AbstractTest { assert !event.getExtensions().isEmpty() } + @Override + protected void applicationStarting(ApplicationStartingEvent event) { + injectorChecks(event) + } + @Override protected void applicationStarted(ApplicationStartedEvent event) { jerseyCheck(event) assert event.jettyStarted + assert event.jerseyStarted assert event.renderJerseyConfig(new JerseyConfig()) != null } @Override - protected void applicationShutdown(ApplicationShotdownEvent event) { + protected void applicationShutdown(ApplicationShutdownEvent event) { jerseyCheck(event) assert event.jettyStarted + assert event.jerseyStarted } @Override @@ -339,6 +351,7 @@ class EventsConsistencyTest extends AbstractTest { private void jerseyCheck(JerseyPhaseEvent event) { injectorChecks(event) assert event.getInjectionManager() != null + assert event.jerseyStarted } } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy index 482d72c78..8ccf77fa7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/LifecycleEventsTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.lifecycle import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyFeatureInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy index 848cad911..400066676 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/MissedEventsTest.groovy @@ -1,13 +1,14 @@ package ru.vyarus.dropwizard.guice.lifecycle -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener import ru.vyarus.dropwizard.guice.module.lifecycle.event.GuiceyLifecycleEvent +import ru.vyarus.dropwizard.guice.module.lifecycle.event.configuration.ConfigurationHooksProcessedEvent import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification @@ -15,7 +16,7 @@ import spock.lang.Specification * @author Vyacheslav Rusakov * @since 24.04.2018 */ -@TestGuiceyApp(App) +@TestGuiceyApp(value = App, useDefaultExtensions = false) class MissedEventsTest extends Specification { def "Check missed events"() { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy index 6b79446c7..af4e47551 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/ShutdownEventTest.groovy @@ -1,12 +1,12 @@ package ru.vyarus.dropwizard.guice.lifecycle -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter -import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShotdownEvent +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationShutdownEvent import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification @@ -22,7 +22,7 @@ class ShutdownEventTest extends Specification { App.stopped = null when: "start-stop with jetty app" - TestSupport.runWebApp(App, null) + TestSupport.runWebApp(App) then: "shutdown called" App.shutdown != null App.shutdown @@ -35,7 +35,7 @@ class ShutdownEventTest extends Specification { App.stopped = null when: "start-stop without jetty app" - TestSupport.runCoreApp(App, null) + TestSupport.runCoreApp(App) then: "shutdown called" App.shutdown != null !App.shutdown // indicate called event, but not started server @@ -53,7 +53,7 @@ class ShutdownEventTest extends Specification { bootstrap.addBundle(GuiceBundle.builder() .listen(new GuiceyLifecycleAdapter() { @Override - protected void applicationShutdown(ApplicationShotdownEvent event) { + protected void applicationShutdown(ApplicationShutdownEvent event) { shutdown = event.jettyStarted } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy index 07d38dcbf..0040c78a1 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/lifecycle/StartedEventTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.lifecycle -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent @@ -20,7 +20,7 @@ class StartedEventTest extends Specification { App.started = null when: "start-stop with jetty app" - TestSupport.runWebApp(App, null) + TestSupport.runWebApp(App) then: "startup called" App.started != null App.started @@ -30,7 +30,7 @@ class StartedEventTest extends Specification { App.started = null when: "start-stop without jetty app" - TestSupport.runCoreApp(App, null) + TestSupport.runCoreApp(App) then: "start called and test env detected" App.started != null !App.started diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/order/InstallersOrderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/order/InstallersOrderTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/order/InstallersOrderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/order/InstallersOrderTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/order/OrderedTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/order/OrderedTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/order/OrderedTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/order/OrderedTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy index 9435324b6..d2225487b 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ExceptionMapperTest.groovy @@ -15,7 +15,7 @@ class ExceptionMapperTest extends AbstractTest { def "Check exception mapper registration"(ClientSupport client) { when: "calling resource which trigger io exception" - def res = client.targetMain("/ex/").request().get() + def res = client.targetApp("/ex/").request().get() then: res.status == 400 res.readEntity(String.class) == 'ERROR: IO exception!' diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy index 6edba0150..20c420db5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedExceptionMapperTest.groovy @@ -15,7 +15,7 @@ class HkManagedExceptionMapperTest extends AbstractTest { def "Check exception mapper registration through hk2"(ClientSupport client) { when: "calling resource which trigger io exception" - def res = client.targetMain("/ex/").request().get() + def res = client.targetApp("/ex/").request().get() then: res.status == 400 res.readEntity(String) == 'ERROR: IO exception!' diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy index f33c96e3e..ba7c280b2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedFactoryTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.provider -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle @@ -12,8 +12,8 @@ import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Supplier /** diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedModelTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedModelTest.groovy new file mode 100644 index 000000000..f1558770d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/HkManagedModelTest.groovy @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.provider + +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo +import ru.vyarus.dropwizard.guice.support.provider.processor.Hk2ManagedModelApp +import ru.vyarus.dropwizard.guice.support.provider.processor.Hk2ManagedProcessor +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2022 + */ +@TestDropwizardApp(Hk2ManagedModelApp) +class HkManagedModelTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check jersey managed processor"() { + + when: "lookup extension info" + ExtensionItemInfo item = info.getInfo(Hk2ManagedProcessor) + then: "jersey managed" + item.isJerseyManaged() + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKResourceTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKResourceTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKResourceTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKResourceTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderHKTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/InjectableProviderTest.groovy diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtInstallByTypeTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtInstallByTypeTest.groovy new file mode 100644 index 000000000..91690b173 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtInstallByTypeTest.groovy @@ -0,0 +1,55 @@ +package ru.vyarus.dropwizard.guice.provider + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +import jakarta.inject.Inject +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2022 + */ +@TestDropwizardApp(App) +class JerseyExtInstallByTypeTest extends AbstractTest { + + @Inject + GuiceyConfigurationInfo info + + def "Check installation without provider annotation"() { + + expect: "extension installed" + info.getExtensions(JerseyProviderInstaller).get(0) == ExM + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(ExM) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } + + static class ExM implements ExceptionMapper { + + @Override + Response toResponse(Throwable throwable) { + return null + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtLegacyBehaviourTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtLegacyBehaviourTest.groovy new file mode 100644 index 000000000..fbb65e047 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/JerseyExtLegacyBehaviourTest.groovy @@ -0,0 +1,53 @@ +package ru.vyarus.dropwizard.guice.provider + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions + +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import ru.vyarus.dropwizard.guice.support.DefaultTestApp + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2022 + */ +class JerseyExtLegacyBehaviourTest extends AbstractTest { + + def "Check installation without provider annotation"() { + + when: "starting" + new App().run("server") + + then: "extension denied" + def ex = thrown(RuntimeException) + ex.cause.message.startsWith('No installer found for extension ru.vyarus.dropwizard.guice.provider.JerseyExtLegacyBehaviourTest$ExM.') + } + + static class App extends DefaultTestApp { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(ExM) + .option(InstallersOptions.JerseyExtensionsRecognizedByType, false) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class ExM implements ExceptionMapper { + + @Override + Response toResponse(Throwable throwable) { + return null + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy index 7a48b8c6b..f0a03e3ea 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/LazyCheckForJerseyItemTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.provider -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.internal.inject.InjectionManager import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle @@ -13,8 +13,8 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.module.installer.install.binding.LazyBinding import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Supplier /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy index b6a1193dd..35a362eec 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/OAuthTest.groovy @@ -5,7 +5,7 @@ import ru.vyarus.dropwizard.guice.support.provider.oauth.OauthCheckApplication import ru.vyarus.dropwizard.guice.test.ClientSupport import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.ws.rs.core.HttpHeaders +import jakarta.ws.rs.core.HttpHeaders /** * @author Vyacheslav Rusakov @@ -17,7 +17,7 @@ class OAuthTest extends AbstractTest { def "Check oath"(ClientSupport client) { when: "calling resource with auth" - def res = client.targetMain("prototype/").request() + def res = client.targetApp("prototype/").request() .header(HttpHeaders.AUTHORIZATION, "Bearer valid").get() then: "user authorized" @@ -25,7 +25,7 @@ class OAuthTest extends AbstractTest { when: "calling resource with invalid auth" res.close() - res = client.targetMain("prototype/").request() + res = client.targetApp("prototype/").request() .header(HttpHeaders.AUTHORIZATION, "Bearer invalid").get() then: "user not authorized" diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy index 5deff400c..4726958fb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/provider/ParamConverterTest.groovy @@ -16,7 +16,7 @@ class ParamConverterTest extends AbstractTest { def "check param converter registration"(ClientSupport client) { when: "calling resource with custom param" - def res = client.targetMain("/param/valllue").request().get() + def res = client.targetApp("/param/valllue").request().get() then: "ok" res.readEntity(Foo).value == 'valllue' } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/request/RequestBeansTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/request/RequestBeansTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/request/RequestBeansTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/request/RequestBeansTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy index fe6fa807d..1efadea0d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ForceSingletonOverrideTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.resource -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.hk2.api.PerLookup import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle @@ -11,8 +11,8 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.module.support.scope.Prototype import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.ws.rs.GET -import javax.ws.rs.Path +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy index e22112b5e..150988521 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/HkManagedResourceTest.groovy @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.resource -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.servlet.http.HttpServletRequest -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Context +import jakarta.servlet.http.HttpServletRequest +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Context import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy index 2f8c1ac4d..b2422449a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/JerseyBindingsTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.resource import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.server.AsyncContext import org.glassfish.jersey.server.ContainerRequest import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider @@ -12,15 +12,15 @@ import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.container.ResourceInfo -import javax.ws.rs.core.HttpHeaders -import javax.ws.rs.core.Request -import javax.ws.rs.core.SecurityContext -import javax.ws.rs.core.UriInfo -import javax.ws.rs.ext.Providers +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.container.ResourceInfo +import jakarta.ws.rs.core.HttpHeaders +import jakarta.ws.rs.core.Request +import jakarta.ws.rs.core.SecurityContext +import jakarta.ws.rs.core.UriInfo +import jakarta.ws.rs.ext.Providers /** * @author Vyacheslav Rusakov @@ -57,7 +57,7 @@ class JerseyBindingsTest extends AbstractTest { @GET String request() { [MultivaluedParameterExtractorProvider, - javax.ws.rs.core.Application, + jakarta.ws.rs.core.Application, Providers, UriInfo, ResourceInfo, diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesNonSingletonTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesNonSingletonTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesNonSingletonTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesNonSingletonTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesSingletonTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesSingletonTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesSingletonTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/resource/ResourcesSingletonTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy index 88712108f..b19932b7c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.util.BindModule import ru.vyarus.dropwizard.guice.test.InjectionTest diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy index bde815cbf..139c0bc2a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutowiredModule.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.support import com.google.inject.AbstractModule -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.support.BootstrapAwareModule import ru.vyarus.dropwizard.guice.module.support.ConfigurationAwareModule import ru.vyarus.dropwizard.guice.module.support.EnvironmentAwareModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy index de77c3858..a0e553740 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomInstallerApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy index c368cb808..d07eb18d4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/CustomModuleApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle /** diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DefaultTestApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DefaultTestApp.java new file mode 100644 index 000000000..a77ced9ca --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DefaultTestApp.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.support; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; + + +/** + * Default test application (without configurations). + * + * @author Vyacheslav Rusakov + * @since 18.02.2025 + */ +public class DefaultTestApp extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(configure()); + } + + protected GuiceBundle configure() { + return GuiceBundle.builder().build(); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + + @Override + protected void onFatalError(Throwable t) { + throw new RuntimeException(t); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy index da0d76f92..e652e5abf 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/DisabledFeatureApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.TaskInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy index 047ec4536..507f50a53 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/ManualApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java new file mode 100644 index 000000000..0af41abe2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.support; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.core.Configuration; + +/** + * Groovy class can't be used anymore, because jackson 2.5 is very sensible for additional methods. + * + * @author Vyacheslav Rusakov + * @since 01.09.2014 + */ +public class TestConfiguration extends Configuration { + + @JsonProperty + public int foo; + + @JsonProperty + public int bar; + + @JsonProperty + public int baa; + + @JsonProperty + public int boo; +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/AutoScanApp2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/AutoScanApp2.groovy new file mode 100644 index 000000000..8282d3f82 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/AutoScanApp2.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.support.auto2 + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.support.TestConfiguration + +/** + * @author Vyacheslav Rusakov + * @since 29.12.2022 + */ +class AutoScanApp2 extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .build() + ); + } + + @Override + void run(TestConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/SampleResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/SampleResource.groovy new file mode 100644 index 000000000..45aa27ee9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/auto2/SampleResource.groovy @@ -0,0 +1,17 @@ +package ru.vyarus.dropwizard.guice.support.auto2 + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 29.12.2022 + */ +@Path("/sample") +class SampleResource { + + @GET + String get() { + return "" + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy index 3bd744f08..8a49c31d2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/badcmd/BadCommand.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.badcmd -import io.dropwizard.cli.Command -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.cli.Command +import io.dropwizard.core.setup.Bootstrap import net.sourceforge.argparse4j.inf.Namespace import net.sourceforge.argparse4j.inf.Subparser diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/client/CustomTestClientFactory.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/client/CustomTestClientFactory.groovy new file mode 100644 index 000000000..08f82082f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/client/CustomTestClientFactory.groovy @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.support.client + +import io.dropwizard.testing.DropwizardTestSupport +import org.glassfish.jersey.client.JerseyClient +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +class CustomTestClientFactory extends DefaultTestClientFactory { + + // not assumed to be used in concurrent tests + static int called + + CustomTestClientFactory() { + called = 0 + } + + @Override + JerseyClient create(DropwizardTestSupport support) { + called++ + return super.create(support) + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/CustomFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/CustomFeature.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/CustomFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/CustomFeature.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java index 2a25e9fec..c6093ba6f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.support.feature; import com.google.inject.Inject; -import io.dropwizard.Application; -import io.dropwizard.cli.EnvironmentCommand; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.cli.EnvironmentCommand; +import io.dropwizard.core.setup.Environment; import net.sourceforge.argparse4j.inf.Namespace; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import ru.vyarus.dropwizard.guice.support.TestConfiguration; diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy index 38c16035d..73d8a3d65 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyExceptionMapper.groovy @@ -3,18 +3,16 @@ package ru.vyarus.dropwizard.guice.support.feature import org.slf4j.Logger import org.slf4j.LoggerFactory -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper /** * http://avianey.blogspot.ru/2011/12/exception-mapping-jersey.html. * @author Vyacheslav Rusakov * @since 03.09.2014 */ -@Provider -@javax.inject.Singleton +@jakarta.inject.Singleton class DummyExceptionMapper implements ExceptionMapper { private final Logger logger = LoggerFactory.getLogger(DummyExceptionMapper.class); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy index 516bc9444..f5004ed37 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyFeature.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.feature -import javax.ws.rs.core.Feature -import javax.ws.rs.core.FeatureContext +import jakarta.ws.rs.core.Feature +import jakarta.ws.rs.core.FeatureContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyHealthCheck.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyHealthCheck.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyHealthCheck.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyHealthCheck.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy index 95940a2ac..39a281530 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyJerseyProvider.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.support.feature -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy index a06965b5c..67df79e91 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyLifeCycle.groovy @@ -48,12 +48,12 @@ class DummyLifeCycle implements LifeCycle { } @Override - void addLifeCycleListener(LifeCycle.Listener listener) { - + boolean addEventListener(EventListener listener) { + return false } @Override - void removeLifeCycleListener(LifeCycle.Listener listener) { - + boolean removeEventListener(EventListener listener) { + return false } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyManaged.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyManaged.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyManaged.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyManaged.groovy diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyModelProcessor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyModelProcessor.groovy new file mode 100644 index 000000000..3d9e6f5ec --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyModelProcessor.groovy @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.support.feature + +import org.glassfish.jersey.server.model.ModelProcessor +import org.glassfish.jersey.server.model.ResourceModel + +import jakarta.inject.Singleton +import jakarta.ws.rs.core.Configuration + +/** + * @author Vyacheslav Rusakov + * @since 04.06.2022 + */ +@Singleton +class DummyModelProcessor implements ModelProcessor { + + @Override + ResourceModel processResourceModel(ResourceModel resourceModel, Configuration configuration) { + return resourceModel + } + + @Override + ResourceModel processSubResource(ResourceModel subResourceModel, Configuration configuration) { + return subResourceModel + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin1.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin1.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyNamedPlugin2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy similarity index 68% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy index 9b7f5be7d..a4c65c18e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyOtherProvider.groovy @@ -1,7 +1,6 @@ package ru.vyarus.dropwizard.guice.support.feature -import javax.ws.rs.ext.ContextResolver -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ContextResolver /** * Check other provider type installation. @@ -9,8 +8,7 @@ import javax.ws.rs.ext.Provider * @author Vyacheslav Rusakov * @since 14.10.2014 */ -@Provider -class DummyOtherProvider implements ContextResolver{ +class DummyOtherProvider implements ContextResolver { @Override Object getContext(Class type) { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin1.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin1.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin3.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin3.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin3.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPlugin3.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPluginKey.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPluginKey.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPluginKey.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyPluginKey.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy index 18990aae0..0a87ec808 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.feature import com.google.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyTask.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyTask.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyTask.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyTask.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy index 3a2e3b8ff..731cf8b53 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleCommand.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.feature import com.google.inject.Inject -import io.dropwizard.cli.ConfiguredCommand -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.cli.ConfiguredCommand +import io.dropwizard.core.setup.Bootstrap import net.sourceforge.argparse4j.inf.Namespace import ru.vyarus.dropwizard.guice.module.installer.scanner.InvisibleForScanner import ru.vyarus.dropwizard.guice.support.TestConfiguration diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy similarity index 79% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy index 2ce3cdf93..1d4ffeb11 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/InvisibleResource.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.feature import ru.vyarus.dropwizard.guice.module.installer.scanner.InvisibleForScanner -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * Checks that invisible annotation handled by installers. diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy index 417a35ed4..20ee86ce8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/NonInjactableCommand.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.feature import com.google.inject.Inject -import io.dropwizard.cli.ConfiguredCommand -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.cli.ConfiguredCommand +import io.dropwizard.core.setup.Bootstrap import net.sourceforge.argparse4j.inf.Namespace import ru.vyarus.dropwizard.guice.support.TestConfiguration diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/PluginInterface2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy index b3bbf5b04..81b5fb5bd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractFeature.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.support.feature.abstr -import javax.ws.rs.core.Feature +import jakarta.ws.rs.core.Feature /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractHealthCheck.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractHealthCheck.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractHealthCheck.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractHealthCheck.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy index 03a2f7632..4ec28c031 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyInjectableProvider.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.feature.abstr -import javax.ws.rs.core.Context +import jakarta.ws.rs.core.Context import java.lang.reflect.Type /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy index 2654d8fb3..bd9449261 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractJerseyProvider.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.support.feature.abstr -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractLifeCycle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractLifeCycle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractLifeCycle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractLifeCycle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractManaged.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractManaged.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractManaged.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractManaged.groovy diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractModelProcessor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractModelProcessor.groovy new file mode 100644 index 000000000..8fa9812e6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractModelProcessor.groovy @@ -0,0 +1,10 @@ +package ru.vyarus.dropwizard.guice.support.feature.abstr + +import org.glassfish.jersey.server.model.ModelProcessor + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2022 + */ +abstract class AbstractModelProcessor implements ModelProcessor { +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractPlugin.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractPlugin.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractPlugin.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractPlugin.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy index f8a8338f7..ffa46a846 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractResource.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.support.feature.abstr -import javax.ws.rs.Path +import jakarta.ws.rs.Path /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractService.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractService.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractService.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractService.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractTask.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractTask.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractTask.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/abstr/AbstractTask.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy index b21677593..8f0a7b077 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/CustomInstaller.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.support.installer -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/InvisibleInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/InvisibleInstaller.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/InvisibleInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installer/InvisibleInstaller.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/DummyInstaller.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/DummyInstaller.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/DummyInstaller.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/DummyInstaller.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy index c99a15aad..968c4ff94 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/installerorder/OrderedInstallersApplication.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.support.installerorder import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.feature.DummyResource diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext1.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext1.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext2.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext2.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext3.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext3.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext3.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/Ext3.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy index 69114c9d2..dd921ce1a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/order/OrderedApplication.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.support.order import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.feature.DummyResource diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy index f5a349de7..ac469c54c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/CustomFeatureInjectableProvider.groovy @@ -2,7 +2,7 @@ package ru.vyarus.dropwizard.guice.support.provider import ru.vyarus.dropwizard.guice.support.feature.CustomFeature -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider import java.util.function.Supplier /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy index 62fe5a059..c2a8e9505 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy index 6998594de..456d52852 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication2.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy index f42d58a5b..36448a6b3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderCheckApplication3.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy similarity index 82% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy index 1af9a2286..f1e92e2d9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderHKManagedResource.groovy @@ -6,11 +6,11 @@ import ru.vyarus.dropwizard.guice.support.feature.CustomFeature import ru.vyarus.dropwizard.guice.support.provider.annotated.Auth import ru.vyarus.dropwizard.guice.support.provider.annotated.User -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov @@ -19,7 +19,7 @@ import javax.ws.rs.core.Response @Path("/prototype") @Produces("application/json") @JerseyManaged -@javax.inject.Singleton +@jakarta.inject.Singleton class InjectableProviderHKManagedResource { @GET diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy index 63a0f44de..bc0e02c43 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/InjectableProviderTestResource.groovy @@ -5,11 +5,11 @@ import ru.vyarus.dropwizard.guice.support.feature.CustomFeature import ru.vyarus.dropwizard.guice.support.provider.annotated.Auth import ru.vyarus.dropwizard.guice.support.provider.annotated.User -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov @@ -17,7 +17,7 @@ import javax.ws.rs.core.Response */ @Path("/prototype") @Produces("application/json") -@javax.inject.Singleton +@jakarta.inject.Singleton class InjectableProviderTestResource { @GET diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy index cdf658b19..67a58ba2c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/LocaleInjectableProvider.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.support.provider -import javax.inject.Inject -import javax.ws.rs.core.HttpHeaders -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.core.HttpHeaders +import jakarta.ws.rs.ext.Provider import java.util.function.Supplier /** @@ -17,10 +17,10 @@ class LocaleInjectableProvider implements Supplier { static int creationCounter = 0 static int callCounter = 0 - private javax.inject.Provider request; + private jakarta.inject.Provider request; @Inject - LocaleInjectableProvider(javax.inject.Provider request) { + LocaleInjectableProvider(jakarta.inject.Provider request) { creationCounter++ this.request = request } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/Auth.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/Auth.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/Auth.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/Auth.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy index a3c90f5cb..bc0fdb3e0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactory.groovy @@ -3,7 +3,7 @@ package ru.vyarus.dropwizard.guice.support.provider.annotated import org.glassfish.jersey.server.ContainerRequest -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy index 4eaea5671..5b093113a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthFactoryProvider.groovy @@ -6,8 +6,8 @@ import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider import org.glassfish.jersey.server.model.Parameter -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** @@ -20,7 +20,7 @@ class AuthFactoryProvider extends AbstractValueParamProvider { Function authFactory; @Inject - public AuthFactoryProvider(final javax.inject.Provider extractorProvider, + public AuthFactoryProvider(final jakarta.inject.Provider extractorProvider, // also Provider could be used final AuthFactory factory) { super(extractorProvider, Parameter.Source.UNKNOWN); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy index a36215dcb..2ad00e2ea 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/AuthInjectionResolver.groovy @@ -5,8 +5,8 @@ import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver import org.glassfish.jersey.server.model.Parameter import org.glassfish.jersey.server.spi.internal.ValueParamProvider -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** @@ -17,7 +17,7 @@ import java.util.function.Function class AuthInjectionResolver extends ParamInjectionResolver { @Inject - AuthInjectionResolver(javax.inject.Provider request, AuthFactory factory) { + AuthInjectionResolver(jakarta.inject.Provider request, AuthFactory factory) { super(new ParamProvider(factory), AuthFactoryProvider, request) } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/User.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/User.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/User.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotated/User.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy index 3f832d57c..d5006d4cb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryHK.groovy @@ -5,7 +5,7 @@ import org.glassfish.jersey.server.ContainerRequest import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.support.provider.annotated.User -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy index 34af5d679..dc0152296 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthFactoryProviderHK.groovy @@ -9,8 +9,8 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged import ru.vyarus.dropwizard.guice.support.provider.annotated.Auth import ru.vyarus.dropwizard.guice.support.provider.annotated.User -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** @@ -25,7 +25,7 @@ class AuthFactoryProviderHK extends AbstractValueParamProvider { Function authFactory; @Inject - public AuthFactoryProviderHK(final javax.inject.Provider extractorProvider, + public AuthFactoryProviderHK(final jakarta.inject.Provider extractorProvider, // also Provider could be used final AuthFactoryHK factory) { super(extractorProvider, Parameter.Source.UNKNOWN); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy index 81651e68a..dd776374c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/annotatedhkmanaged/AuthInjectionResolverHK.groovy @@ -9,8 +9,8 @@ import ru.vyarus.dropwizard.guice.support.provider.annotated.Auth import ru.vyarus.dropwizard.guice.support.provider.annotated.AuthFactory import ru.vyarus.dropwizard.guice.support.provider.annotated.AuthFactoryProvider -import javax.inject.Inject -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.ws.rs.ext.Provider import java.util.function.Function /** @@ -21,7 +21,7 @@ import java.util.function.Function @JerseyManaged class AuthInjectionResolverHK extends ParamInjectionResolver { @Inject - AuthInjectionResolverHK(javax.inject.Provider request, AuthFactory factory) { + AuthInjectionResolverHK(jakarta.inject.Provider request, AuthFactory factory) { super(new ParamProvider(factory), AuthFactoryProvider, request) } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy index 64cbf8eb4..6ca7cd58f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider.exceptionmapper -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy index 92aab3298..a70f935b3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionMapperApp2.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider.exceptionmapper -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy index be1e35ad9..e2041db94 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/ExceptionResource.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.provider.exceptionmapper import com.google.inject.Singleton -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy index df6dd1fce..5e7c4c0b8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/HkManagedExceptionMapper.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.support.provider.exceptionmapper import com.google.inject.Singleton import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy index fe5d558e2..a9ff98120 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/exceptionmapper/IOExceptionMapper.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.provider.exceptionmapper import com.google.inject.Singleton -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy index aa14530e4..f7cdf11fc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthDynamicFeature.groovy @@ -2,14 +2,15 @@ package ru.vyarus.dropwizard.guice.support.provider.oauth import io.dropwizard.auth.* import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature -import javax.inject.Inject -import javax.inject.Singleton -import javax.ws.rs.core.Feature -import javax.ws.rs.core.FeatureContext -import javax.ws.rs.ext.Provider +import jakarta.inject.Inject +import jakarta.inject.Singleton +import jakarta.ws.rs.container.ContainerRequestContext +import jakarta.ws.rs.core.Feature +import jakarta.ws.rs.core.FeatureContext +import jakarta.ws.rs.ext.Provider /** * @author Vyacheslav Rusakov @@ -34,7 +35,7 @@ class OAuthDynamicFeature extends AuthDynamicFeature { // may be external class (internal for simplicity) @Singleton - public static class OAuthAuthenticator implements Authenticator { + static class OAuthAuthenticator implements Authenticator { @Override Optional authenticate(String credentials) throws AuthenticationException { @@ -44,15 +45,16 @@ class OAuthDynamicFeature extends AuthDynamicFeature { // may be external class (internal for simplicity) @Singleton - public static class OAuthAuthorizer implements Authorizer { + static class OAuthAuthorizer implements Authorizer { + @Override - public boolean authorize(User user, String role) { + boolean authorize(User user, String role, ContainerRequestContext requestContext) { return user.getName().equals("good-guy") && role.equals("ADMIN"); } } // will be installed by JerseyFeatureInstaller - public static class ConfigurationFeature implements Feature { + static class ConfigurationFeature implements Feature { @Override boolean configure(FeatureContext context) { context.register(RolesAllowedDynamicFeature.class) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy index eabb18ecd..7a4e90893 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OAuthTestResource.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.support.provider.oauth import com.google.common.base.Preconditions import io.dropwizard.auth.Auth -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov @@ -14,7 +14,7 @@ import javax.ws.rs.core.Response */ @Path("/prototype") @Produces("application/json") -@javax.inject.Singleton +@jakarta.inject.Singleton class OAuthTestResource { @GET diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy index 633468f66..5c56f7319 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/OauthCheckApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider.oauth -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/User.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/User.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/User.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/oauth/User.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/Foo.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/Foo.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/Foo.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/Foo.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy index 610b33885..27f7ff804 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/FooParamConverter.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider.paramconv -import javax.ws.rs.ext.ParamConverter -import javax.ws.rs.ext.ParamConverterProvider -import javax.ws.rs.ext.Provider +import jakarta.ws.rs.ext.ParamConverter +import jakarta.ws.rs.ext.ParamConverterProvider +import jakarta.ws.rs.ext.Provider import java.lang.annotation.Annotation import java.lang.reflect.Type diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy index 54114c882..e03c5b8fe 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamConverterApp.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.provider.paramconv -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.provider.JerseyProviderInstaller diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy similarity index 71% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy index 3fa8d57f8..78ec68d76 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/paramconv/ParamResource.groovy @@ -4,13 +4,13 @@ import ru.vyarus.dropwizard.guice.support.feature.CustomFeature import ru.vyarus.dropwizard.guice.support.provider.annotated.Auth import ru.vyarus.dropwizard.guice.support.provider.annotated.User -import javax.inject.Singleton -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.Produces -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response +import jakarta.inject.Singleton +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.Response /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedModelApp.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedModelApp.groovy new file mode 100644 index 000000000..9b9e074d4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedModelApp.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.support.provider.processor + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2022 + */ +class Hk2ManagedModelApp extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(Hk2ManagedProcessor) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedProcessor.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedProcessor.groovy new file mode 100644 index 000000000..85899731d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/provider/processor/Hk2ManagedProcessor.groovy @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.support.provider.processor + +import org.glassfish.jersey.server.model.ModelProcessor +import org.glassfish.jersey.server.model.ResourceModel +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged + +import jakarta.ws.rs.core.Configuration + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2022 + */ +@JerseyManaged +class Hk2ManagedProcessor implements ModelProcessor { + + @Override + ResourceModel processResourceModel(ResourceModel resourceModel, Configuration configuration) { + return resourceModel + } + + @Override + ResourceModel processSubResource(ResourceModel subResourceModel, Configuration configuration) { + return subResourceModel + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy index 8ebd60767..351025b09 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansApplication.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.request -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.util.BindModule diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy index 807a0d0c0..b5f8c9144 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestBeansResource.groovy @@ -4,12 +4,12 @@ import com.google.common.base.Preconditions import com.google.inject.Inject import com.google.inject.Provider -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * Rest service for validation of guice request scoped beans support. diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedBean.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedBean.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedBean.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedBean.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy index d0dc6aad1..aba64b941 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/request/RequestScopedDependencyTask.groovy @@ -5,8 +5,8 @@ import com.google.inject.Inject import com.google.inject.Provider import io.dropwizard.servlets.tasks.Task -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * Just to show that this WILL NOT work, because guice filter registered only for jersey context. diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy index 5dcf38d8a..f53f154be 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/PrototypeResource.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.support.resource import com.google.inject.Inject import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * Resource will be instantiated for evert request. diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy index 0f08bd500..b532ae98e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/ResourceSingletonCheckApplication.groovy @@ -3,9 +3,9 @@ package ru.vyarus.dropwizard.guice.support.resource import com.google.inject.AbstractModule import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.feature.DummyService diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy index e3eccb219..b1bcc07e3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/resource/SingletonResource.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.support.resource import com.google.inject.Inject import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.Produces -import javax.ws.rs.core.Response +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response /** * Single resource instance will be used for all requests. @@ -16,7 +16,7 @@ import javax.ws.rs.core.Response */ @Path("/singleton") @Produces('application/json') -@javax.inject.Singleton +@jakarta.inject.Singleton class SingletonResource { static int creationCounter = 0 static int callCounter = 0 diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy index 31961ac8b..93a1d1901 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyFilter.groovy @@ -4,18 +4,18 @@ import com.google.common.base.Preconditions import com.google.inject.Inject import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.servlet.Filter -import javax.servlet.FilterChain -import javax.servlet.FilterConfig -import javax.servlet.ServletException -import javax.servlet.ServletRequest -import javax.servlet.ServletResponse +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.FilterConfig +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse /** * @author Vyacheslav Rusakov * @since 12.10.2014 */ -@javax.inject.Singleton +@jakarta.inject.Singleton class DummyFilter implements Filter { DummyService service diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy index afd4b1dd5..66feb04c3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/DummyServlet.groovy @@ -4,16 +4,16 @@ import com.google.common.base.Preconditions import com.google.inject.Inject import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.servlet.ServletException -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * @author Vyacheslav Rusakov * @since 12.10.2014 */ -@javax.inject.Singleton +@jakarta.inject.Singleton class DummyServlet extends HttpServlet { DummyService service diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy index 71c984196..11c5574f7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/ServletsApplication.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.support.web import com.google.inject.Binder import com.google.inject.Module -import io.dropwizard.Application -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.TestConfiguration import ru.vyarus.dropwizard.guice.support.feature.DummyService diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/WebModule.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/WebModule.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/WebModule.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/WebModule.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy index 4fa19a28d..d6ece6ea7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextFilter.groovy @@ -2,8 +2,8 @@ package ru.vyarus.dropwizard.guice.support.web.crosscontext import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.* -import javax.servlet.annotation.WebFilter +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy similarity index 78% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy index 5956e1548..9c34831c4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextListener.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.web.crosscontext import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.ServletRequestEvent -import javax.servlet.ServletRequestListener -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpServletRequest +import jakarta.servlet.ServletRequestEvent +import jakarta.servlet.ServletRequestListener +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpServletRequest /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy similarity index 68% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy index 0b07c922d..b63ae2a52 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/crosscontext/CrossContextServlet.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.support.web.crosscontext import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.ServletException -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy similarity index 69% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy index a35f66993..b94e88ba3 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminFilter.groovy @@ -2,13 +2,13 @@ package ru.vyarus.dropwizard.guice.support.web.feature import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.Filter -import javax.servlet.FilterChain -import javax.servlet.FilterConfig -import javax.servlet.ServletException -import javax.servlet.ServletRequest -import javax.servlet.ServletResponse -import javax.servlet.annotation.WebFilter +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.FilterConfig +import jakarta.servlet.ServletException +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy similarity index 67% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy index d6048e6e5..e76b964f7 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/AdminServlet.groovy @@ -2,11 +2,11 @@ package ru.vyarus.dropwizard.guice.support.web.feature import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.ServletException -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy index 54ec225bd..06c48c4d9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyFilter.groovy @@ -3,9 +3,9 @@ package ru.vyarus.dropwizard.guice.support.web.feature import com.google.common.base.Preconditions import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.inject.Inject -import javax.servlet.* -import javax.servlet.annotation.WebFilter +import jakarta.inject.Inject +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy similarity index 71% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy index fbb123d3b..a66dfdb93 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyServlet.groovy @@ -3,12 +3,12 @@ package ru.vyarus.dropwizard.guice.support.web.feature import com.google.common.base.Preconditions import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.inject.Inject -import javax.servlet.ServletException -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.inject.Inject +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy similarity index 74% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy index 43dfcced3..1115b5e17 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/DummyWebListener.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.support.web.feature import ru.vyarus.dropwizard.guice.support.feature.DummyService -import javax.inject.Inject -import javax.servlet.ServletRequestEvent -import javax.servlet.ServletRequestListener -import javax.servlet.annotation.WebListener +import jakarta.inject.Inject +import jakarta.servlet.ServletRequestEvent +import jakarta.servlet.ServletRequestListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy index 8fbc462a3..78489f982 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/FilterOnServlet.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.feature -import javax.servlet.* -import javax.servlet.annotation.WebFilter +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter /** * Filter mapped on admin servlet. diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy similarity index 72% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy index fea06653c..d33d1202d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractFilter.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.feature.abstr -import javax.servlet.Filter -import javax.servlet.annotation.WebFilter +import jakarta.servlet.Filter +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy index 39a0c0bee..e25d7b375 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractServlet.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.feature.abstr -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy similarity index 69% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy index 4ce245711..9c9efe7dc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/feature/abstr/AbstractWebListener.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.feature.abstr -import javax.servlet.ServletRequestListener -import javax.servlet.annotation.WebListener +import jakarta.servlet.ServletRequestListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy index d844e5d1d..2e619a607 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/CompositeListener.groovy @@ -1,12 +1,12 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.ServletContextEvent -import javax.servlet.ServletContextListener -import javax.servlet.ServletRequestAttributeEvent -import javax.servlet.ServletRequestAttributeListener -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionEvent -import javax.servlet.http.HttpSessionListener +import jakarta.servlet.ServletContextEvent +import jakarta.servlet.ServletContextListener +import jakarta.servlet.ServletRequestAttributeEvent +import jakarta.servlet.ServletRequestAttributeListener +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpSessionEvent +import jakarta.servlet.http.HttpSessionListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy index 7dbaf7f9f..6ef9d1324 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextAttributeListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.ServletContextAttributeEvent -import javax.servlet.ServletContextAttributeListener -import javax.servlet.annotation.WebListener +import jakarta.servlet.ServletContextAttributeEvent +import jakarta.servlet.ServletContextAttributeListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy index fe366c600..a601c4980 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/ContextListener.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.support.web.listeners import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.ServletContextEvent -import javax.servlet.ServletContextListener -import javax.servlet.annotation.WebListener +import jakarta.servlet.ServletContextEvent +import jakarta.servlet.ServletContextListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy index ffdadb364..15d1d62fe 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestAttributeListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.ServletRequestAttributeEvent -import javax.servlet.ServletRequestAttributeListener -import javax.servlet.annotation.WebListener +import jakarta.servlet.ServletRequestAttributeEvent +import jakarta.servlet.ServletRequestAttributeListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy index 6dffeab6c..f250b6489 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/RequestListener.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.support.web.listeners import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext -import javax.servlet.ServletRequestEvent -import javax.servlet.ServletRequestListener -import javax.servlet.annotation.WebListener +import jakarta.servlet.ServletRequestEvent +import jakarta.servlet.ServletRequestListener +import jakarta.servlet.annotation.WebListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy similarity index 74% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy index 285a0b4a8..97d7dac0d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionAttributeListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionAttributeListener -import javax.servlet.http.HttpSessionBindingEvent +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpSessionAttributeListener +import jakarta.servlet.http.HttpSessionBindingEvent /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy similarity index 67% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy index f03742fd3..e4d03554d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionIdListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionEvent -import javax.servlet.http.HttpSessionIdListener +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpSessionEvent +import jakarta.servlet.http.HttpSessionIdListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy similarity index 70% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy index b7a4ffa38..9765da5f5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/listeners/SessionListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.listeners -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionEvent -import javax.servlet.http.HttpSessionListener +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpSessionEvent +import jakarta.servlet.http.HttpSessionListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy index 81363f1d5..a49dd8f30 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsFilter.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.params -import javax.servlet.* -import javax.servlet.annotation.WebFilter -import javax.servlet.annotation.WebInitParam +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter +import jakarta.servlet.annotation.WebInitParam /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy index 4a31b9859..dbc38d132 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/InitParamsServlet.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.support.web.params -import javax.servlet.ServletConfig -import javax.servlet.ServletException -import javax.servlet.annotation.WebInitParam -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet +import jakarta.servlet.ServletConfig +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebInitParam +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy index 5e5ebe07c..154cde75a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/params/ServletRegFilter.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.params -import javax.servlet.* -import javax.servlet.annotation.WebFilter +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy similarity index 67% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy index b92834a10..d41744c2a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet1.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.servletclash -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy similarity index 67% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy index 531445a74..1036258b4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/servletclash/Servlet2.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support.web.servletclash -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy similarity index 66% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy index 95ba26a3a..279f9f4bc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/support/web/session/SessionListener.groovy @@ -1,8 +1,8 @@ package ru.vyarus.dropwizard.guice.support.web.session -import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionEvent -import javax.servlet.http.HttpSessionIdListener +import jakarta.servlet.annotation.WebListener +import jakarta.servlet.http.HttpSessionEvent +import jakarta.servlet.http.HttpSessionIdListener /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy index 08704ba88..c389ba426 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.eclipse.jetty.util.component.LifeCycle import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp @@ -34,7 +34,7 @@ class LifecycleStartedForGuiceyTest extends Specification { @Override void run(Configuration configuration, Environment environment) throws Exception { - environment.lifecycle().addLifeCycleListener(new LifeCycle.Listener() { + environment.lifecycle().addEventListener(new LifeCycle.Listener() { @Override void lifeCycleStarted(LifeCycle event) { called = true diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy index 76033dd3a..2a716d86e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/bind/OverridingBindingsOverrideTest.groovy @@ -1,16 +1,16 @@ package ru.vyarus.dropwizard.guice.test.bind import com.google.inject.AbstractModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.binding.BindingsOverrideInjectorFactory import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/BaseClientTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/BaseClientTest.java new file mode 100644 index 000000000..03749198d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/BaseClientTest.java @@ -0,0 +1,81 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.builder.util.VoidBodyReader; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.Resource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 13.10.2025 + */ +@TestDropwizardApp(ClientApp.class) +public class BaseClientTest { + + @Test + void testBaseMethods(ClientSupport client) { + final TestClient app = client.appClient(); + assertThat(app.toString()).isEqualTo("Client for: http://localhost:8080/"); + assertThat(app.getBaseUri().toString()).isEqualTo("http://localhost:8080/"); + + final List called = new ArrayList<>(); + + app.defaultPathConfiguration(webTarget -> { + called.add("called"); + return webTarget; + }); + + // defaults not applied + app.target("/root/get").request().get(); + assertThat(called).isEmpty(); + + // defaults applied + app.request("/root/get").get(); + assertThat(called).containsExactly("called"); + + called.clear(); + app.subClient(builder -> builder.path("/root/")).get("/get"); + assertThat(called).containsExactly("called"); + + called.clear(); + app.subClient(builder -> builder.path("/root/"), Resource.class).get("/get"); + assertThat(called).containsExactly("called"); + + called.clear(); + app.subClient("/root/").asRestClient(Resource.class).get("/get"); + assertThat(called).containsExactly("called"); + } + + @Test + void testArrayQueryParams(ClientSupport client) { + final TestClient app = client.appClient(); + app.defaultQueryParam("q", new Object[]{"1", "2"}); + + app.buildGet("/root/get") + .assertRequest(tracker -> + assertThat(tracker.getUrl()).endsWith("?q=1&q=2")) + .asVoid(); + + app.reset().defaultMatrixParam("m", new Object[]{"1", "2"}); + + app.buildGet("/root/get") + .assertRequest(tracker -> + assertThat(tracker.getUrl()).endsWith(";m=1;m=2")) + .asVoid(); + } + + @Test + void testNullExtension(ClientSupport client) { + final TestClient app = client.appClient(); + app.defaultRegister(VoidBodyReader.class, null); + + app.buildGet("/root/get").asVoid(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientDefaultsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientDefaultsTest.java new file mode 100644 index 000000000..a6188fad2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientDefaultsTest.java @@ -0,0 +1,213 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.builder.TestRequestConfig; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 07.10.2025 + */ +@TestGuiceyApp(value = ClientApp.class, apacheClient = true) +public class ClientDefaultsTest { + + final Function pathFunc = target -> target.path("foo"); + final Consumer reqFunc = req -> req.header("foo", "bar"); + final Object extension = new Object(); + final SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy"); + final CacheControl cacheControl = RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString("max-age=604800, must-revalidate"); + + @Test + void testDefaults(ClientSupport support) { + support.defaultHeader("A", "a") + .defaultHeader(HttpHeader.ACCESS_CONTROL_MAX_AGE, "12") + .defaultHeader("B", () -> 11) + .defaultHeader(HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS, () -> 13) + .defaultQueryParam("q", 1) + .defaultQueryParam("q2", () -> 2) + .defaultMatrixParam("m", 1) + .defaultMatrixParam("m2", () -> 2) + .defaultPathParam("p", 1) + .defaultPathParam("p2", () -> 2) + .defaultProperty("PR", 1) + .defaultProperty("PR2", () -> 2) + .defaultRegister(Integer.class) + .defaultRegister(extension) + .defaultRegister(String.class, () -> "12") + .defaultFormDateFormatter(formatter) + .defaultFormDateTimeFormatter(DateTimeFormatter.ISO_DATE) + .defaultCookie("c1", "1") + .defaultCookie(new NewCookie.Builder("c2").value("11").build()) + .defaultCookie("c3", () -> new NewCookie.Builder("c3").value("12").build()) + .defaultAccept("application/json") + .defaultLanguage("en") + .defaultEncoding("gzip") + .defaultCacheControl("max-age=604800, must-revalidate") + .defaultDebug(true) + .defaultPathConfiguration(pathFunc) + .defaultRequestConfiguration(reqFunc); + + support.printDefaults(); + final TestRequestConfig defaults = support.getDefaults(); + assertThat(defaults.getConfiguredHeadersMap()).hasSize(4) + .containsEntry("A", "a") + .containsEntry("B", 11) + .containsEntry(HttpHeader.ACCESS_CONTROL_MAX_AGE.asString(), "12") + .containsEntry(HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS.asString(), 13); + + assertThat(defaults.getConfiguredQueryParamsMap()).hasSize(2) + .containsEntry("q", 1) + .containsEntry("q2", 2); + + assertThat(defaults.getConfiguredMatrixParamsMap()).hasSize(2) + .containsEntry("m", 1) + .containsEntry("m2", 2); + + assertThat(defaults.getConfiguredPathParamsMap()).hasSize(2) + .containsEntry("p", 1) + .containsEntry("p2", 2); + + assertThat(defaults.getConfiguredPropertiesMap()).hasSize(2) + .containsEntry("PR", 1) + .containsEntry("PR2", 2); + + assertThat(defaults.getConfiguredExtensionsMap()).hasSize(3) + .containsEntry(Integer.class, Integer.class) + .containsEntry(Object.class, extension) + .containsEntry(String.class, "12"); + + assertThat(defaults.getConfiguredFormDateFormatter()).isEqualTo(formatter); + assertThat(defaults.getConfiguredFormDateTimeFormatter()).isEqualTo(DateTimeFormatter.ISO_DATE); + + assertThat(defaults.getConfiguredCookiesMap()).hasSize(3) + .containsEntry("c1", new NewCookie.Builder("c1").value("1").build()) + .containsEntry("c2", new NewCookie.Builder("c2").value("11").build()) + .containsEntry("c3", new NewCookie.Builder("c3").value("12").build()); + + assertThat(defaults.getConfiguredAccepts()).hasSize(1).containsOnly("application/json"); + assertThat(defaults.getConfiguredLanguages()).hasSize(1).containsOnly("en"); + assertThat(defaults.getConfiguredEncodings()).hasSize(1).containsOnly("gzip"); + assertThat(defaults.getConfiguredCacheControl()).isEqualTo(cacheControl); + assertThat(defaults.getConfiguredPathModifiers()).hasSize(1).element(0).isEqualTo(pathFunc); + assertThat(defaults.getConfiguredRequestModifiers()).hasSize(1).element(0).isEqualTo(reqFunc); + assertThat(defaults.isDebugEnabled()).isTrue(); + } + + @Test + void testCustomDefaults(ClientSupport support) { + support.defaultAccept(MediaType.TEXT_HTML_TYPE) + .defaultLanguage(Locale.CANADA) + .defaultCacheControl(cacheControl); + + final TestRequestConfig defaults = support.getDefaults(); + assertThat(defaults.getConfiguredAccepts()).hasSize(1).containsOnly(MediaType.TEXT_HTML); + assertThat(defaults.getConfiguredLanguages()).hasSize(1).containsOnly(Locale.CANADA.toString()); + assertThat(defaults.getConfiguredCacheControl()).isEqualTo(cacheControl); + } + + @Test + void testDefaultFlags(ClientSupport support) { + assertThat(support.hasDefaultHeaders()).isFalse(); + support.defaultHeader("A", "a"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultHeaders()).isTrue(); + + assertThat(support.reset().hasDefaultQueryParams()).isFalse(); + support.defaultQueryParam("q", 1); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultQueryParams()).isTrue(); + + assertThat(support.reset().hasDefaultMatrixParams()).isFalse(); + support.defaultMatrixParam("m", 1); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultMatrixParams()).isTrue(); + + assertThat(support.reset().hasDefaultPathParams()).isFalse(); + support.defaultPathParam("p", 1); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultPathParams()).isTrue(); + + assertThat(support.reset().hasDefaultProperties()).isFalse(); + support.reset().defaultProperty("PR", 1); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultProperties()).isTrue(); + + assertThat(support.reset().hasDefaultExtensions()).isFalse(); + support.defaultRegister(Integer.class); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultExtensions()).isTrue(); + + assertThat(support.reset().hasDefaultFormDateFormatter()).isFalse(); + support.reset().defaultFormDateFormatter(formatter); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultFormDateFormatter()).isTrue(); + + assertThat(support.reset().hasDefaultFormDateFormatter()).isFalse(); + support.reset().defaultFormDateTimeFormatter(DateTimeFormatter.ISO_DATE); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultFormDateFormatter()).isTrue(); + + assertThat(support.reset().hasDefaultCookies()).isFalse(); + support.defaultCookie("c1", "1"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultCookies()).isTrue(); + + assertThat(support.reset().hasDefaultAccepts()).isFalse(); + support.defaultAccept("application/json"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultAccepts()).isTrue(); + + assertThat(support.reset().hasDefaultLanguages()).isFalse(); + support.defaultLanguage("en"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultLanguages()).isTrue(); + + assertThat(support.reset().hasDefaultEncodings()).isFalse(); + support.defaultEncoding("gzip"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultEncodings()).isTrue(); + + assertThat(support.reset().hasDefaultCacheControl()).isFalse(); + support.defaultCacheControl("max-age=604800, must-revalidate"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultCacheControl()).isTrue(); + + assertThat(support.reset().hasDefaultCustomConfigurators()).isFalse(); + support.defaultPathConfiguration(pathFunc); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultCustomConfigurators()).isTrue(); + + assertThat(support.reset().hasDefaultCustomConfigurators()).isFalse(); + support.defaultRequestConfiguration(reqFunc); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultCustomConfigurators()).isTrue(); + + assertThat(support.reset().isDebugEnabled()).isFalse(); + support.defaultDebug(true); + assertThat(support.hasDefaults()).isFalse(); + assertThat(support.isDebugEnabled()).isTrue(); + + assertThat(support.reset().hasDefaultFormDateFormatter()).isFalse(); + support.defaultFormDateFormat("yyyy"); + assertThat(support.hasDefaults()).isTrue(); + assertThat(support.hasDefaultFormDateFormatter()).isTrue(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientRequestBuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientRequestBuilderTest.java new file mode 100644 index 000000000..b3d2bda82 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientRequestBuilderTest.java @@ -0,0 +1,90 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.core.GenericType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientResponse; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.SuccFailRedirectResource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 08.10.2025 + */ +@TestDropwizardApp(value = ClientApp.class, apacheClient = true) +public class ClientRequestBuilderTest { + + @Test + void testInvocations(ClientSupport client) { + final ResourceClient rest = client.restClient(SuccFailRedirectResource.class); + + rest.method(SuccFailRedirectResource::get).asVoid(); + assertThat(rest.method(SuccFailRedirectResource::get).as(String.class)).isEqualTo("ok"); + assertThat(rest.method(SuccFailRedirectResource::get).as(new GenericType() {})).isEqualTo("ok"); + + TestClientResponse response = rest.method(SuccFailRedirectResource::get).invoke(); + assertThat(response.as(String.class)).isEqualTo("ok"); + + response = rest.method("post", "test").invoke(); + assertThat(response.as(String.class)).isEqualTo("test"); + } + + @Test + void testStatusCheck(ClientSupport client) { + final ResourceClient rest = client.restClient(SuccFailRedirectResource.class); + + // WHEN success - expect success + TestClientResponse response = rest.method(SuccFailRedirectResource::get).expectSuccess(); + assertThat(response.asResponse().getStatus()).isEqualTo(200); + + // WHEN success - expect failure + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::get).expectFailure()) + .isInstanceOf(AssertionError.class) + .hasMessage("Failed response expected, but found 'SUCCESSFUL' ==> expected: not equal but was: "); + + // WHEN success 200 - expect 201 + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::get).expectSuccess(201)) + .isInstanceOf(AssertionError.class) + .hasMessage("Unexpected response status 200 when expected 201 ==> expected: but was: "); + + + // WHEN failure - expect failure + response = rest.method(SuccFailRedirectResource::error).expectFailure(); + assertThat(response.asResponse().getStatus()).isEqualTo(500); + + // WHEN failure = expect success + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::error).expectSuccess()) + .isInstanceOf(InternalServerErrorException.class) + .hasMessage("HTTP 500 Server Error"); + + // WHEN failure 500 - expect 401 + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::error).expectFailure(401)) + .isInstanceOf(AssertionError.class) + .hasMessage("Unexpected response status 500 when expected 401 ==> expected: but was: "); + + // WHEN redirect - expect redirect + response = rest.method(SuccFailRedirectResource::redirect).expectRedirect(); + assertThat(response.asResponse().getStatus()).isEqualTo(303); + assertThat(response.asResponse().getHeaderString("Location")).isNotBlank(); + + // WHEN no redirect - expect redirect + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::get).expectRedirect()) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected 'REDIRECTION' response status, but found 'SUCCESSFUL' ==> expected: but was: "); + + // WHEN error - expect redirect + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::error).expectRedirect()) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected 'REDIRECTION' response status, but found 'SERVER_ERROR' ==> expected: but was: "); + + // WHEN redirect 303 - expect redirect 301 + Assertions.assertThatThrownBy(() -> rest.method(SuccFailRedirectResource::redirect).expectRedirect(301)) + .isInstanceOf(AssertionError.class) + .hasMessage("Unexpected response status 303 when expected 301 ==> expected: but was: "); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientResponseTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientResponseTest.java new file mode 100644 index 000000000..957962a46 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientResponseTest.java @@ -0,0 +1,281 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.builder.TestClientResponse; +import ru.vyarus.dropwizard.guice.test.client.util.FileDownloadUtil; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.MultipartSupport; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.FileResource; +import ru.vyarus.dropwizard.guice.test.client.support.Resource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 08.10.2025 + */ +@TestDropwizardApp(value = ClientApp.class, apacheClient = true) +public class ClientResponseTest { + + @TempDir + Path temp; + + @Test + void testResponseAssertions(ClientSupport client) { + + final ResourceClient rest = client.restClient(Resource.class); + + final TestClientResponse response = rest.method(Resource::get).invoke(); + response.assertResponse(res -> res.getStatus() == 200); + assertThat(response.toString()).isEqualTo("Response: 200 OK"); + + assertThatThrownBy(() -> response.assertResponse(res -> res.getStatus() == 500)) + .isInstanceOf(AssertionError.class) + .hasMessage("Response does not match condition ==> expected: but was: "); + + assertThatThrownBy(() -> response.assertResponse(List.class, res -> res.size() == 1)) + .isInstanceOf(AssertionError.class) + .hasMessage("Response does not match condition ==> expected: but was: "); + + // response body was already read + final TestClientResponse response2 = rest.method(Resource::get).invoke(); + assertThatThrownBy(() -> response2.assertResponse(new GenericType>() {}, res -> res.size() == 1)) + .isInstanceOf(AssertionError.class) + .hasMessage("Response does not match condition ==> expected: but was: "); + + assertThatThrownBy(() -> response2.assertStatus(300)) + .isInstanceOf(AssertionError.class) + .hasMessage("Unexpected response status 200 when expected 300 ==> expected: but was: "); + + assertThatThrownBy(() -> response2.assertStatus(Response.Status.Family.CLIENT_ERROR)) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected 'CLIENT_ERROR' response status, but found 'SUCCESSFUL' ==> expected: but was: "); + + assertThatThrownBy(() -> response2.assertStatus(statusType -> statusType.getStatusCode() == 300)) + .isInstanceOf(AssertionError.class) + .hasMessage("Response status '200 OK' does not match condition ==> expected: but was: "); + + response2.assertSuccess().assertStatus(200); + + assertThatThrownBy(response2::assertFail) + .isInstanceOf(AssertionError.class) + .hasMessage("Failed response expected, but found 'SUCCESSFUL' ==> expected: not equal but was: "); + + assertThatThrownBy(response2::assertRedirect) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected 'REDIRECTION' response status, but found 'SUCCESSFUL' ==> expected: but was: "); + + // response body was already read + final TestClientResponse response3 = rest.method(Resource::get).invoke(); + + assertThatThrownBy(response3::assertVoidResponse) + .isInstanceOf(AssertionError.class) + .hasMessage("Void response expected, but found: \n[1,2,3] ==> expected: but was: "); + + // response body was already read + final TestClientResponse response4 = rest.method(Resource::del).invoke(); + response4.assertVoidResponse(); + + + final TestClientResponse response5 = rest.method(Resource::get).invoke(); + response5.assertResponse(String.class, "[1,2,3]"::equals); + + final TestClientResponse response6 = rest.method(Resource::get).invoke(); + response6.assertResponse(new GenericType() {}, "[1,2,3]"::equals); + } + + @Test + void testResponseClose(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + + final TestClientResponse response7 = rest.method(Resource::get).invoke(); + response7.close(); + assertThatThrownBy(() -> response7.as(String.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Entity input stream has already been closed."); + } + + @Test + void testResponseHeadersAssertions(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + + final TestClientResponse response = rest.method(Resource::filled).invoke(); + + // WHEN success checks + response.assertCacheControl(CacheControl::isMustRevalidate) + .assertHeader("HH", "3") + .assertHeader(HttpHeader.X_POWERED_BY, "4") + .assertHeader("HH", "3"::equals) + .assertHeader(HttpHeader.X_POWERED_BY, "4"::equals) + .assertMedia(MediaType.TEXT_PLAIN_TYPE) + .assertLocale(Locale.CANADA) + .assertCookie("C", "12") + .assertCookie("C", cookie -> cookie.getValue().equals("12")); + + // WHEN failed assertions + assertThatThrownBy(() -> response.assertHeader("Unknw", "12")) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Missing header 'Unknw' in response. Available headers: "); + + assertThatThrownBy(() -> response.assertHeader("HH", "4")) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Expected header 'HH' value '4', but found '3'"); + + assertThatThrownBy(() -> response.assertHeader(HttpHeader.ACCEPT_LANGUAGE, s -> s.equals("11"))) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Missing header 'Accept-Language' in response. Available headers: "); + + assertThatThrownBy(() -> response.assertHeader(HttpHeader.X_POWERED_BY, s -> "5".equals(s))) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Header 'X-Powered-By: 4' does not match condition"); + + assertThatThrownBy(() -> response.assertMedia(MediaType.APPLICATION_JSON_TYPE)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Expected 'application/json' media type, but found 'text/plain'"); + + assertThatThrownBy(() -> response.assertLocale(Locale.CHINA)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Expected 'zh_CN' response locale, but found 'en_CA'"); + + assertThatThrownBy(() -> response.assertCookie("Unkwn", "1")) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Missing cookie 'Unkwn' in response. Available cookies:"); + + assertThatThrownBy(() -> response.assertCookie("C", "1")) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Expected cookie 'C: 1', but found '12'"); + + assertThatThrownBy(() -> response.assertCookie("Unkwn", cookie -> cookie.getValue().equals("1"))) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Missing cookie 'Unkwn' in response. Available cookies:"); + + assertThatThrownBy(() -> response.assertCookie("C", cookie -> cookie.getValue().equals("1"))) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Cookie '$Version=1;C=12' does not match condition"); + } + + @Test + void testWithMethods(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + + final TestClientResponse response = rest.method(Resource::filled).invoke(); + + // WHEN with* checks + response.withHeader("HH", s -> assertThat(s).isEqualTo("3")); + response.withHeader(HttpHeader.X_POWERED_BY, s -> assertThat(s).isEqualTo("4")); + response.withStatus(statusType -> assertThat(statusType.getStatusCode()).isEqualTo(200)); + response.withCacheControl(cc -> assertThat(cc.isMustRevalidate()).isTrue()); + response.withCookie("C", cookie -> assertThat(cookie.getValue()).isEqualTo("12")); + response.withResponse(res -> assertThat(res.getStatus()).isEqualTo(200)); + response.withResponse(String.class, s -> assertThat(s).isEqualTo("OK")); + + // reset body + final TestClientResponse response2 = rest.method(Resource::filled).invoke(); + response2.withResponse(new GenericType() {}, s -> assertThat(s).isEqualTo("OK")); + + // reset body + final TestClientResponse response3 = rest.method(Resource::filled).invoke(); + + // WHEN with* fail + assertThatThrownBy(() -> response3.withHeader("HH", s -> + assertThat(s).as("Bad HH header").isEqualTo("4"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Bad HH header"); + + assertThatThrownBy(() -> response3.withHeader(HttpHeader.X_POWERED_BY, s -> + assertThat(s).as("Bad header").isEqualTo("5"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Bad header"); + + assertThatThrownBy(() -> response3.withStatus(s -> + assertThat(s.getFamily()).as("Bad status").isEqualTo(Response.Status.Family.CLIENT_ERROR))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Bad status"); + + assertThatThrownBy(() -> response3.withCacheControl(cc -> + assertThat(assertThat(cc.isMustRevalidate()).as("Cache revalidation enabled").isFalse()))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Cache revalidation enabled"); + + assertThatThrownBy(() -> response3.withCookie("C", c -> + assertThat(assertThat(c.getValue()).as("Cookie C invalid").isEqualTo("1")))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Cookie C invalid"); + + assertThatThrownBy(() -> response3.withResponse(res -> + assertThat(assertThat(res.getStatus()).as("Bad status").isEqualTo(201)))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Bad status"); + + assertThatThrownBy(() -> response3.withResponse(new GenericType() {}, res -> + assertThat(assertThat(res).as("Bad response").isEqualTo("KO")))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Bad response"); + } + + @Test + void testResponseMappings(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + + assertThat(rest.method(Resource::get).invoke() + .as(List.class)).hasSize(3).contains(1, 2, 3); + assertThat(rest.method(Resource::get).invoke() + .as(new GenericType>() {})).hasSize(3).contains(1, 2, 3); + assertThat((Integer) rest.method(Resource::get).invoke() + .as(response -> response.readEntity(new GenericType>() {}).get(0))).isEqualTo(1); + assertThat(rest.method(Resource::get).invoke().asResponse().getStatus()).isEqualTo(200); + + assertThat(rest.method(Resource::get).invoke().asList(Integer.class)).containsExactly(1, 2, 3); + + } + + @Test + void testFileDownload(ClientSupport client) { + final ResourceClient rest = client.restClient(FileResource.class); + + // WHEN jersey file download (temp) + File res = rest.method(FileResource::download).as(File.class); + System.out.println(res.getAbsolutePath()); + String tmp = System.getProperty("java.io.tmpdir"); + assertThat(res.getAbsolutePath()).startsWith(tmp); + assertThat(res.getName()).isNotEqualTo("logback.xml"); + + // WHEN file download api + Path file = rest.method(FileResource::download).invoke().asFile(temp); + assertThat(file.getParent()).isEqualTo(temp); + assertThat(file.getFileName().toString()).isEqualTo("logback.xml"); + + // WHEN same file download (name counter appears) + file = rest.method(FileResource::download).invoke().asFile(temp); + assertThat(file.getFileName().toString()).isEqualTo("logback(1).xml"); + + // WHEN raw response + Response response = rest.method(FileResource::download).invoke().asResponse(); + assertThat(MultipartSupport.readFilename(response)).isEqualTo("logback.xml"); + assertThat(FileDownloadUtil.parseFileName(response)).isEqualTo("logback.xml"); + + // WHEN non file response + response = client.restClient(Resource.class).method(instance -> instance.get()).invoke().asResponse(); + assertThat(MultipartSupport.readFilename(response)).isNull(); + assertThat(FileDownloadUtil.parseFileName(response)).isNull(); + + // WHEN incorrect header + assertThatThrownBy(() -> MultipartSupport.readFilename("incorrect header")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to parse " + HttpHeader.CONTENT_DISPOSITION + " header"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientTypeChangeTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientTypeChangeTest.java new file mode 100644 index 000000000..b839b42aa --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ClientTypeChangeTest.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 13.10.2025 + */ +@TestDropwizardApp(ClientApp.class) +public class ClientTypeChangeTest { + + @Test + void testUrlconnectionClientFailure(ClientSupport client) { + ClientSupport support = client.urlconnectorClient(); + assertThat(support.getClient().getConfiguration().getConnectorProvider().getClass()) + .isEqualTo(HttpUrlConnectorProvider.class); + + support = client.apacheClient(); + assertThat(support.getClient().getConfiguration().getConnectorProvider().getClass()) + .isEqualTo(Apache5ConnectorProvider.class); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ErrorsForExpectSuccessTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ErrorsForExpectSuccessTest.java new file mode 100644 index 000000000..f06f6d02a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/ErrorsForExpectSuccessTest.java @@ -0,0 +1,101 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotAcceptableException; +import jakarta.ws.rs.NotAllowedException; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.NotSupportedException; +import jakarta.ws.rs.RedirectionException; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.ServiceUnavailableException; +import jakarta.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.ErrorsResource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2025 + */ +@TestDropwizardApp(ClientApp.class) +public class ErrorsForExpectSuccessTest { + // expectSuccess re-implements jersey exceptions throwing logic - check it's unified with jersey behaviour + + @Test + public void testErrors(ClientSupport client) { + final ResourceClient rest = client.restClient(ErrorsResource.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::bad).as(String.class)) + .isInstanceOf(BadRequestException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::bad).expectSuccess()) + .isInstanceOf(BadRequestException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::unauth).as(String.class)) + .isInstanceOf(NotAuthorizedException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::unauth).expectSuccess()) + .isInstanceOf(NotAuthorizedException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::forbid).as(String.class)) + .isInstanceOf(ForbiddenException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::forbid).expectSuccess()) + .isInstanceOf(ForbiddenException.class); + + assertThatThrownBy(() -> rest.buildGet("/notexistingmethod").as(String.class)) + .isInstanceOf(NotFoundException.class); + assertThatThrownBy(() -> rest.buildGet("/notexistingmethod").expectSuccess()) + .isInstanceOf(NotFoundException.class); + + assertThatThrownBy(() -> rest.buildDelete("/bad").as(String.class)) + .isInstanceOf(NotAllowedException.class); + assertThatThrownBy(() -> rest.buildDelete("/bad").expectSuccess()) + .isInstanceOf(NotAllowedException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::notacc).as(String.class)) + .isInstanceOf(NotAcceptableException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::notacc).expectSuccess()) + .isInstanceOf(NotAcceptableException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::unsupported).as(String.class)) + .isInstanceOf(NotSupportedException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::unsupported).expectSuccess()) + .isInstanceOf(NotSupportedException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::error).as(String.class)) + .isInstanceOf(InternalServerErrorException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::error).expectSuccess()) + .isInstanceOf(InternalServerErrorException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::unavailable).as(String.class)) + .isInstanceOf(ServiceUnavailableException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::unavailable).expectSuccess()) + .isInstanceOf(ServiceUnavailableException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::customClient).as(String.class)) + .isInstanceOf(ClientErrorException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::customClient).expectSuccess()) + .isInstanceOf(ClientErrorException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::customServer).as(String.class)) + .isInstanceOf(ServerErrorException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::customServer).expectSuccess()) + .isInstanceOf(ServerErrorException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::customRedirect).as(String.class)) + .isInstanceOf(RedirectionException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::customRedirect).expectSuccess()) + .isInstanceOf(RedirectionException.class); + + assertThatThrownBy(() -> rest.method(ErrorsResource::informal).as(String.class)) + .isInstanceOf(WebApplicationException.class); + assertThatThrownBy(() -> rest.method(ErrorsResource::informal).expectSuccess()) + .isInstanceOf(WebApplicationException.class); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientFormsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientFormsTest.java new file mode 100644 index 000000000..4c51c3e1d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientFormsTest.java @@ -0,0 +1,252 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.FormBeanResource; +import ru.vyarus.dropwizard.guice.test.client.support.FormResource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.io.File; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 10.10.2025 + */ +@TestDropwizardApp(value = ClientApp.class) +public class RestClientFormsTest { + + @Test + void testUrlencodedForms(ClientSupport client) { + final ResourceClient rest = client.restClient(FormResource.class); + + // WHEN simple values + assertThat(rest.buildForm("/post") + .param("name", "1") + .param("date", "2") + .buildPost() + .as(String.class)).isEqualTo("name=1, date=2"); + + assertThat(rest.method(instance -> instance.post("1", "2")) + .as(String.class)).isEqualTo("name=1, date=2"); + + // WHEN multimap parameter + assertThat(rest.buildForm("/post2") + .param("name", "1") + .param("date", "2") + .buildPost() + .as(String.class)).contains("name=[1]").contains("date=[2]"); + + final MultivaluedHashMap map = new MultivaluedHashMap<>(); + map.add("name", "1"); + map.add("date", "2"); + assertThat(rest.method(instance -> instance.post2(map)) + .as(String.class)).contains("name=[1]").contains("date=[2]"); + + // WHEN multivalue + assertThat(rest.buildForm("/postMulti") + .param("name", 1, 2, 3) + .param("date", "2") + .buildPost() + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + + assertThat(rest.method(instance -> instance.postMulti(Arrays.asList("1", "2", "3"), "2")) + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + + // WHEN multimap parameter with multivalues + assertThat(rest.buildForm("/post2Multi") + .param("name", 1, 2, 3) + .param("date", "2") + .buildPost() + .as(String.class)).contains("name=[1, 2, 3]").contains("date=[2]"); + + final MultivaluedHashMap map2 = new MultivaluedHashMap<>(); + map2.addAll("name", "1", "2", "3"); + map2.add("date", "2"); + assertThat(rest.method(instance -> instance.post2Multi(map2)) + .as(String.class)).contains("name=[1, 2, 3]").contains("date=[2]"); + } + + @Test + void testUrlencodedWithBeans(ClientSupport client) { + final ResourceClient rest = client.restClient(FormBeanResource.class); + + // WHEN simple values + assertThat(rest.buildForm("/post") + .param("name", "1") + .param("date", "2") + .buildPost() + .as(String.class)).isEqualTo("name=1, date=2"); + + assertThat(rest.method(instance -> instance.post(new FormBeanResource.SimpleBean("1", "2"))) + .as(String.class)).isEqualTo("name=1, date=2"); + + + // WHEN multivalue + assertThat(rest.buildForm("/postMulti") + .param("name", 1, 2, 3) + .param("date", "2") + .buildPost() + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + + assertThat(rest.method(instance -> instance.postMulti(new FormBeanResource.SimpleMultiBean(Arrays.asList("1", "2", "3"), "2"))) + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + } + + + @Test + void testGet(ClientSupport client) { + final ResourceClient rest = client.restClient(FormResource.class); + + // WHEN simple values + assertThat(rest.buildForm("/get") + .param("name", "1") + .param("date", "2") + .buildGet() + .as(String.class)).isEqualTo("name=1, date=2"); + + assertThat(rest.method(instance -> instance.get("1", "2")) + .as(String.class)).isEqualTo("name=1, date=2"); + + // WHEN multivalue + assertThat(rest.buildForm("/getMulti") + .param("name", 1, 2, 3) + .param("date", "2") + .buildGet() + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + + assertThat(rest.method(instance -> instance.getMulti(Arrays.asList("1", "2", "3"), "2")) + .as(String.class)).isEqualTo("name=[1, 2, 3], date=2"); + } + + @Test + void testMultipart(ClientSupport client) { + final ResourceClient rest = client.restClient(FormResource.class); + + // WHEN 2 args + assertThat(rest.buildForm("/multipart") + .param("file", new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart(multipart.fromClasspath("/logback.xml"), + multipart.disposition("file", "logback.xml"))) + .as(String.class)).isEqualTo("logback.xml"); + + + // WHEN body param arg + assertThat(rest.buildForm("/multipart2") + .param("file", new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart2(multipart.streamPart("file", "/logback.xml"))) + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart2(multipart.filePart("file", "src/test/resources/logback.xml"))) + .as(String.class)).isEqualTo("logback.xml"); + + + // WHEN multiple params with the same name + assertThat(rest.buildForm("/multipartMulti") + .param("file", new File("src/test/resources/logback.xml"), new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipartMulti(Arrays.asList( + multipart.filePart("file", "src/test/resources/logback.xml"), + multipart.filePart("file", "src/test/resources/logback.xml")))) + .as(String.class)).isEqualTo("logback.xml"); + + // WHEN multiple dispositions with the same name + assertThat(rest.buildForm("/multipartMulti2") + .param("file", new File("src/test/resources/logback.xml"), new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipartMulti2(Arrays.asList( + multipart.disposition("file", "logback.xml"), + multipart.disposition("file", "logback.xml")))) + .as(String.class)).isEqualTo("logback.xml"); + + // WHEN generic multipart used + assertThat(rest.buildForm("/multipartGeneric") + .param("file", new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipartGeneric(multipart.multipart() + .field("foo", "bar") + .stream("file", "/logback.xml") + .build())) + .as(String.class)).isEqualTo("logback.xml"); + } + + @Test + void testMultipartWithBeans(ClientSupport client) { + final ResourceClient rest = client.restClient(FormBeanResource.class); + + // WHEN 2 args + assertThat(rest.buildForm("/multipart") + .param("file", new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart(new FormBeanResource.MultipartBean( + multipart.fromClasspath("/logback.xml"), + multipart.disposition("file", "logback.xml")))) + .as(String.class)).isEqualTo("logback.xml"); + + + // WHEN body param arg + assertThat(rest.buildForm("/multipart2") + .param("file", new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart2(new FormBeanResource.MultipartBean2(multipart.streamPart("file", "/logback.xml")))) + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipart2(new FormBeanResource.MultipartBean2(multipart.filePart("file", "src/test/resources/logback.xml")))) + .as(String.class)).isEqualTo("logback.xml"); + + + // WHEN multiple params with the same name + assertThat(rest.buildForm("/multipartMulti") + .param("file", new File("src/test/resources/logback.xml"), new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipartMulti(new FormBeanResource.MultipartMultiBean(Arrays.asList( + multipart.filePart("file", "src/test/resources/logback.xml"), + multipart.filePart("file", "src/test/resources/logback.xml"))))) + .as(String.class)).isEqualTo("logback.xml"); + + // WHEN multiple dispositions with the same name + assertThat(rest.buildForm("/multipartMulti2") + .param("file", new File("src/test/resources/logback.xml"), new File("src/test/resources/logback.xml")) + .buildPost() + .as(String.class)).isEqualTo("logback.xml"); + + assertThat(rest.multipartMethod((instance, multipart) -> + instance.multipartMulti2(new FormBeanResource.MultipartMultiBean2(Arrays.asList( + multipart.disposition("file", "logback.xml"), + multipart.disposition("file", "logback.xml"))))) + .as(String.class)).isEqualTo("logback.xml"); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientTest.java new file mode 100644 index 000000000..096a28e86 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/RestClientTest.java @@ -0,0 +1,194 @@ +package ru.vyarus.dropwizard.guice.test.client; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.GenericType; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.support.ClientApp; +import ru.vyarus.dropwizard.guice.test.client.support.MatrixResource; +import ru.vyarus.dropwizard.guice.test.client.support.PrimitivesResource; +import ru.vyarus.dropwizard.guice.test.client.support.Resource; +import ru.vyarus.dropwizard.guice.test.client.support.sub.SubMatrix; +import ru.vyarus.dropwizard.guice.test.client.support.sub.SubResource; +import ru.vyarus.dropwizard.guice.test.client.support.sub.SubSubResource; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 07.10.2025 + */ +@TestDropwizardApp(value = ClientApp.class, apacheClient = true) +public class RestClientTest { + + @Test + @SuppressWarnings("unchecked") + void testRestClient(ClientSupport client) throws Exception { + final ResourceClient rest = client.restClient(Resource.class); + assertThat(rest.toString()).isEqualTo("Rest client for: Resource (http://localhost:8080/root)"); + + // WHEN method string + assertThat(rest.method("get").as(List.class)).containsExactly(1, 2, 3); + assertThat(rest.method(Resource.class.getMethod("get")).as(List.class)).containsExactly(1, 2, 3); + + // WHEN method instance + assertThat(rest.method(Resource.class.getMethod("get", String.class)) + .pathParam("name", "1") + .as(List.class)).containsExactly(4, 5, 6); + + // WHEN method string with body + assertThat(rest.method("post", "sample").asString()).isEqualTo("sample"); + + // WHEN method instance with body + assertThat(rest.method(Resource.class.getMethod("post", String.class), Entity.text("sample")).asString()).isEqualTo("sample"); + assertThat(rest.method(Resource.class.getMethod("post", String.class), Entity.text("sample")).asString()).isEqualTo("sample"); + } + + @Test + void testBodyAsArgument(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + + // WHEN body as argument + final Resource.ModelType body = new Resource.ModelType("test"); + assertThat(rest.method(instance -> instance.post2(body)).asString()).isEqualTo("test"); + + // WHEN annotated body + assertThat(rest.method(instance -> instance.post3(body)).asString()).isEqualTo("test"); + } + + @Test + void testSubResources(ClientSupport client) { + final ResourceClient rest = client.restClient(Resource.class); + assertThat(rest.method(instance -> instance.sub().get()).asString()).isEqualTo("ok"); + assertThat(rest.subResourceClient("sub", SubResource.class) + .method(SubResource::get) + .asString()) + .isEqualTo("ok"); + + assertThat(rest.subResourceClient("sub", SubResource.class) + .subResourceClient("sub2", SubSubResource.class) + .method(SubSubResource::get).asString()).isEqualTo("ko"); + + assertThat(rest.subResourceClient(Resource::sub, SubResource.class) + .subResourceClient("sub2", SubSubResource.class) + .method(SubSubResource::get).asString()).isEqualTo("ko"); + + assertThatThrownBy(() -> rest.restClient(SubResource.class)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("In context of resource, sub-resource client should be obtained with " + + "subResourceClient() method which ignores sub-resource @Path annotation (not used in sub-resource path building)"); + } + + @Test + void testMatrixParams(ClientSupport client) { + final ResourceClient rest = client.restClient(MatrixResource.class); + + assertThat(rest.method(instance -> instance.get("1", "2")) + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("matrix/get;p1=1;p2=2")) + .asString()).isEqualTo("1;2"); + + assertThat(rest.method(instance -> instance.get(null, "1", "2")) + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("matrix/get2;s=1/op;p1=1;p2=2")) + .pathParam("vars", "get2;s=1") + .asString()).isEqualTo("1;2"); + + assertThat(rest.method(instance -> instance.sub(null).get("2")) + .assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("matrix/sub;p1=1/get;s1=2")) + .pathParam("vars", "sub;p1=1") + .asString()).isEqualTo("2"); + } + + @Test + void testBasicRestClient(ClientSupport client) { + final TestClient rest = client.restClient(); + assertThat(rest.toString()).isEqualTo("Client for: http://localhost:8080/"); + + assertThat(rest.restClient(Resource.class).method(Resource::get).asString()).isEqualTo("[1,2,3]"); + + assertThat(rest.subClient("matrix/sub;p1=1").asRestClient(SubMatrix.class) + .method(instance -> instance.get("2")).asString()) + .isEqualTo("2"); + + assertThat(rest.subClient(uriBuilder -> uriBuilder.path("matrix/sub").matrixParam("p1", 1), SubMatrix.class) + .method(instance -> instance.get("2")).asString()) + .isEqualTo("2"); + + assertThat(rest.subClient(uriBuilder -> uriBuilder.path("matrix/{sub}") + .resolveTemplate("sub", "sub") + .matrixParam("p1", 1), SubMatrix.class) + .method(instance -> instance.get("2")).asString()) + .isEqualTo("2"); + } + + @Test + void testShortcuts(ClientSupport client) { + final TestClient rest = client.restClient().subClient("/root"); + + rest.get("/%s", "get"); + assertThat(rest.get("/%s", List.class, "get")).isEqualTo(Arrays.asList(1, 2, 3)); + assertThat(rest.get("/%s", new GenericType>() {}, "get")).isEqualTo(Arrays.asList(1, 2, 3)); + assertThat(rest.buildGet("/%s", "get").as(List.class)).isEqualTo(Arrays.asList(1, 2, 3)); + + rest.delete("/%s", "delete"); + assertThat(rest.delete("/%s", Integer.class, "delete")).isEqualTo(1); + assertThat(rest.delete("/%s", new GenericType() {}, "delete")).isEqualTo(1); + assertThat(rest.buildDelete("/%s", "delete").as(Integer.class)).isEqualTo(1); + + rest.post("/%s", "text", "post"); + rest.post("/%s", Entity.text("text"), "post"); + assertThat(rest.post("/%s", "text", String.class, "post")).isEqualTo("text"); + assertThat(rest.post("/%s", Entity.text("text"), String.class, "post")).isEqualTo("text"); + assertThat(rest.post("/%s", "text", new GenericType() {}, "post")).isEqualTo("text"); + assertThat(rest.post("/%s", Entity.text("text"), new GenericType() {}, "post")).isEqualTo("text"); + assertThat(rest.buildPost("/%s", "text", "post").asString()).isEqualTo("text"); + assertThat(rest.buildPost("/%s", Entity.text("text"), "post").asString()).isEqualTo("text"); + + rest.put("/%s", "text", "put"); + rest.put("/%s", Entity.text("text"), "put"); + assertThat(rest.put("/%s", "text", String.class, "put")).isEqualTo("text"); + assertThat(rest.put("/%s", Entity.text("text"), String.class, "put")).isEqualTo("text"); + assertThat(rest.put("/%s", "text", new GenericType() {}, "put")).isEqualTo("text"); + assertThat(rest.put("/%s", Entity.text("text"), new GenericType() {}, "put")).isEqualTo("text"); + assertThat(rest.buildPut("/%s", "text", "put").asString()).isEqualTo("text"); + assertThat(rest.buildPut("/%s", Entity.text("text"), "put").asString()).isEqualTo("text"); + + rest.patch("/%s", "text", "patch"); + rest.patch("/%s", Entity.text("text"), "patch"); + assertThat(rest.patch("/%s", "text", String.class, "patch")).isEqualTo("text"); + assertThat(rest.patch("/%s", Entity.text("text"), String.class, "patch")).isEqualTo("text"); + assertThat(rest.patch("/%s", "text", new GenericType() {}, "patch")).isEqualTo("text"); + assertThat(rest.patch("/%s", Entity.text("text"), new GenericType() {}, "patch")).isEqualTo("text"); + assertThat(rest.buildPatch("/%s", "text", "patch").asString()).isEqualTo("text"); + assertThat(rest.buildPatch("/%s", Entity.text("text"), "patch").asString()).isEqualTo("text"); + } + + @Test + void testPrimitiveResponses(ClientSupport client) { + final ResourceClient rest = client.restClient(PrimitivesResource.class); + + assertThat(rest.method(PrimitivesResource::getByte).asString()).isEqualTo("1"); + assertThat(rest.method(PrimitivesResource::getBytes).as(byte[].class)).isEqualTo(new byte[]{1}); + assertThat(rest.method(PrimitivesResource::getLong).asString()).isEqualTo("1"); + assertThat(rest.method(PrimitivesResource::getLong).asString()).isEqualTo("1"); + assertThat(rest.method(PrimitivesResource::getBoolean).asString()).isEqualTo("false"); + assertThat(rest.method(PrimitivesResource::getShort).asString()).isEqualTo("1"); + assertThat(rest.method(PrimitivesResource::getFloat).asString()).isEqualTo("1.0"); + assertThat(rest.method(PrimitivesResource::getChar).as(char.class)).isEqualTo((char) 1); + assertThat(rest.method(PrimitivesResource::getInt).asString()).isEqualTo("1"); + assertThat(rest.method(PrimitivesResource::getDouble).asString()).isEqualTo("1.0"); + } + + @Test + void testManualRestCall(ClientSupport client) { + assertThat(client.appClient() + .subClient("/root") + .asRestClient(Resource.class) + .get("/get", String.class)) + .isEqualTo("[1,2,3]"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilderTest.java new file mode 100644 index 000000000..4b6adf811 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/FormBuilderTest.java @@ -0,0 +1,234 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MultivaluedMap; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +public class FormBuilderTest { + + @Test + void testSimpleFormBuild() { + FormBuilder builder = new FormBuilder(new TargetMock(), null); + + builder.param("foo", "bar") + .param("bar", "baz") + .param("tt", 12); + + assertThat(builder.toString()).isEqualTo("Form builder for: "); + + + // WHEN build entity + Entity entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(Form.class); + final MultivaluedMap map = ((Form) entity.getEntity()).asMap(); + assertThat(map).hasSize(3) + .containsEntry("foo", List.of("bar")) + .containsEntry("bar", List.of("baz")) + .containsEntry("tt", List.of("12")); + + // WHEN build query params + final Map res = builder.buildQueryParams(); + assertThat(res).hasSize(3) + .containsEntry("foo", "bar") + .containsEntry("bar", "baz") + .containsEntry("tt", "12"); + + // WHEN build multipart + entity = builder.forceMultipart().buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + final FormDataMultiPart body = (FormDataMultiPart) entity.getEntity(); + assertThat(body.getBodyParts()).hasSize(3); + assertThat(body.getField("foo").getValue()).isEqualTo("bar"); + assertThat(body.getField("bar").getValue()).isEqualTo("baz"); + assertThat(body.getField("tt").getValue()).isEqualTo("12"); + } + + @Test + void testMultiValueParameter() { + FormBuilder builder = new FormBuilder(new TargetMock(), null); + + builder.param("foo", "bar", "baz") + .param("foo2", Arrays.asList("bar", "baz")) + .param("tt", 11, 12) + .param("tt2", (Object) new Integer[]{11, 12}); + + // WHEN build entity + Entity entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(Form.class); + final MultivaluedMap map = ((Form) entity.getEntity()).asMap(); + assertThat(map).hasSize(4) + .containsEntry("foo", List.of("bar", "baz")) + .containsEntry("foo2", List.of("bar", "baz")) + .containsEntry("tt", List.of("11", "12")) + .containsEntry("tt2", List.of("11", "12")); + + // WHEN build query params + final Map res = builder.buildQueryParams(); + assertThat(res).hasSize(4) + .containsEntry("foo", List.of("bar", "baz")) + .containsEntry("foo2", List.of("bar", "baz")) + .containsEntry("tt", List.of("11", "12")) + .containsEntry("tt", List.of("11", "12")); + + // WHEN build multipart + entity = builder.forceMultipart().buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + final FormDataMultiPart body = (FormDataMultiPart) entity.getEntity(); + assertThat(body.getBodyParts()).hasSize(8); + assertThat(body.getFields().get("foo").size()).isEqualTo(2); + assertThat(body.getFields().get("foo")).extracting(FormDataBodyPart::getValue).containsExactly("bar", "baz"); + assertThat(body.getFields().get("foo2")).extracting(FormDataBodyPart::getValue).containsExactly("bar", "baz"); + assertThat(body.getFields().get("tt")).extracting(FormDataBodyPart::getValue).containsExactly("11", "12"); + assertThat(body.getFields().get("tt2")).extracting(FormDataBodyPart::getValue).containsExactly("11", "12"); + } + + @Test + void testConfigurationFromMap() { + FormBuilder builder = new FormBuilder(new TargetMock(), null); + + builder.params(ImmutableMap.builder() + .put("foo", "bar") + .put("foo2", Arrays.asList("bar", "baz")) + .put("tt", 11) + .put("tt2", new Integer[]{11, 12}) + .build()); + + // WHEN build entity + Entity entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(Form.class); + final MultivaluedMap map = ((Form) entity.getEntity()).asMap(); + assertThat(map).hasSize(4) + .containsEntry("foo", List.of("bar")) + .containsEntry("foo2", List.of("bar", "baz")) + .containsEntry("tt", List.of("11")) + .containsEntry("tt2", List.of("11", "12")); + + // WHEN build query params + final Map res = builder.buildQueryParams(); + assertThat(res).hasSize(4) + .containsEntry("foo", "bar") + .containsEntry("foo2", List.of("bar", "baz")) + .containsEntry("tt", "11") + .containsEntry("tt2", List.of("11", "12")); + + // WHEN build multipart + entity = builder.forceMultipart().buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + final FormDataMultiPart body = (FormDataMultiPart) entity.getEntity(); + assertThat(body.getBodyParts()).hasSize(6); + assertThat(body.getFields().get("foo")).extracting(FormDataBodyPart::getValue).containsExactly("bar"); + assertThat(body.getFields().get("foo2")).extracting(FormDataBodyPart::getValue).containsExactly("bar", "baz"); + assertThat(body.getFields().get("tt")).extracting(FormDataBodyPart::getValue).containsExactly("11"); + assertThat(body.getFields().get("tt2")).extracting(FormDataBodyPart::getValue).containsExactly("11", "12"); + } + + @Test + void testMultipartFormRecognition() { + FormBuilder builder = new FormBuilder(new TargetMock(), null); + final File file = new File("src/test/resources/test.txt"); + builder.param("file", file); + + // WHEN build simple entity + Entity entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + assertThat(((FormDataMultiPart) entity.getEntity()).getField("file")) + .isInstanceOf(FileDataBodyPart.class) + .extracting(part -> ((FileDataBodyPart) part).getFileEntity()).isEqualTo(file); + + // WHEN build from stream + builder = new FormBuilder(new TargetMock(), null); + InputStream stream = new ByteArrayInputStream(new byte[0]); + builder.param("file", stream); + entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + assertThat(((FormDataMultiPart) entity.getEntity()).getField("file")) + .isInstanceOf(StreamDataBodyPart.class) + .extracting(part -> ((StreamDataBodyPart) part).getStreamEntity()).isEqualTo(stream); + + // WHEN build from body part + builder = new FormBuilder(new TargetMock(), null); + FileDataBodyPart bodyPart = new FileDataBodyPart("file", file); + builder.param("file", bodyPart); + entity = builder.buildEntity(); + assertThat(entity.getEntity()).isInstanceOf(FormDataMultiPart.class); + assertThat(((FormDataMultiPart) entity.getEntity()).getField("file")) + .isInstanceOf(FileDataBodyPart.class) + .isEqualTo(bodyPart); + } + + @Test + void testDateFormatters() throws Exception { + // WHEN direct pattern + FormBuilder builder = new FormBuilder(new TargetMock(), null) + .dateFormat("yyyy"); + + builder.param("util", new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2012")) + .param("time", DateTimeFormatter.ofPattern("dd/MM/yyyy").parse("11/11/2011")); + + assertThat(builder.buildQueryParams()).hasSize(2) + .containsEntry("util", "2012") + .containsEntry("time", "2011"); + + // WHEN direct separate objects + builder = new FormBuilder(new TargetMock(), null) + .dateFormatter(new SimpleDateFormat("yyyy")) + .dateTimeFormatter(DateTimeFormatter.ofPattern("yyyy")); + + builder.param("util", new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2012")) + .param("time", DateTimeFormatter.ofPattern("dd/MM/yyyy").parse("11/11/2011")); + + assertThat(builder.buildQueryParams()).hasSize(2) + .containsEntry("util", "2012") + .containsEntry("time", "2011"); + + + // WHEN pattern from config + TestRequestConfig config = new TestRequestConfig(null) + .formDateFormatter(new SimpleDateFormat("yyyy")) + .formDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy")); + builder = new FormBuilder(new TargetMock(), config); + + builder.param("util", new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2012")) + .param("time", DateTimeFormatter.ofPattern("dd/MM/yyyy").parse("11/11/2011")); + + assertThat(builder.buildQueryParams()).hasSize(2) + .containsEntry("util", "2012") + .containsEntry("time", "2011"); + + // WHEN pattern from default + TestClient client = new TestClient<>(TargetMock::new, null); + client.defaultFormDateFormat("yyyy"); + + + assertThat(client.buildForm("/test") + .param("util", new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2012")) + .param("time", DateTimeFormatter.ofPattern("dd/MM/yyyy").parse("11/11/2011")) + .buildQueryParams()) + .hasSize(2) + .containsEntry("util", "2012") + .containsEntry("time", "2011"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/RequestBuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/RequestBuilderTest.java new file mode 100644 index 000000000..73620ccfa --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/RequestBuilderTest.java @@ -0,0 +1,186 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.message.internal.TracingLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock; +import ru.vyarus.dropwizard.guice.test.client.builder.util.VoidBodyReader; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 08.10.2025 + */ +public class RequestBuilderTest { + + final Function pathFunc = target -> target.path("foo"); + final Consumer reqFunc = req -> req.header("foo", "bar"); + final Object extension = new Object(); + final CacheControl cacheControl = RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString("max-age=604800, must-revalidate"); + + + @BeforeEach + void setUp() { + TestSupportHolder.reset(); + } + + @Test + void testRequestBuilder() { + List called = new ArrayList<>(); + TestClientRequestBuilder builder = new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .configurePath(pathFunc) + .configureRequest(reqFunc) + .notFollowRedirects() + .noBodyMappingForVoid() + .accept("application/json") + .acceptLanguage("en") + .acceptEncoding("gzip") + .queryParam("q1", "1") + .queryParams(ImmutableMap.of("q2", 2, "q3", 3)) + .matrixParam("m1", "1") + .matrixParams(ImmutableMap.of("m2", 2, "m3", 3)) + .pathParam("p1", 1) + .pathParams(ImmutableMap.of("p2", 2, "p3", 3)) + .header(HttpHeader.ACCESS_CONTROL_MAX_AGE, "11") + .header("A", "a") + .headers(ImmutableMap.of("B", "b", "C", 12)) + .cookie("c1", "1") + .cookie(new NewCookie.Builder("c2").value("11").build()) + .cookies(ImmutableMap.of("c3", "3", "c4", "4")) + .property("prop1", "foo") + .properties(ImmutableMap.of("prop2", "bar", "prop3", "baz")) + .register(Integer.class) + .register(extension) + .cacheControl("max-age=604800, must-revalidate") + .enableJerseyTrace() + .assertRequest(tracker -> + called.add("called")); + + TestRequestConfig config = builder.getConfig(); + assertThat(config.getConfiguredPathModifiers()).hasSize(1).element(0).isEqualTo(pathFunc); + assertThat(config.getConfiguredRequestModifiers()).hasSize(1).element(0).isEqualTo(reqFunc); + assertThat(config.getConfiguredAccepts()).hasSize(1).containsOnly("application/json"); + assertThat(config.getConfiguredLanguages()).hasSize(1).containsOnly("en"); + assertThat(config.getConfiguredEncodings()).hasSize(1).containsOnly("gzip"); + + assertThat(config.getConfiguredQueryParamsMap()).hasSize(3) + .containsEntry("q1", "1") + .containsEntry("q2", 2) + .containsEntry("q3", 3); + + assertThat(config.getConfiguredMatrixParamsMap()).hasSize(3) + .containsEntry("m1", "1") + .containsEntry("m2", 2) + .containsEntry("m3", 3); + + assertThat(config.getConfiguredPathParamsMap()).hasSize(3) + .containsEntry("p1", 1) + .containsEntry("p2", 2) + .containsEntry("p3", 3); + + assertThat(config.getConfiguredHeadersMap()).hasSize(6) + .containsEntry("A", "a") + .containsEntry("B", "b") + .containsEntry("C", 12) + .containsEntry(HttpHeader.ACCESS_CONTROL_MAX_AGE.asString(), "11") + .containsEntry(TracingLogger.HEADER_ACCEPT, "true") + .containsEntry(TracingLogger.HEADER_THRESHOLD, TracingLogger.Level.SUMMARY.name()); + + assertThat(config.getConfiguredCookiesMap()).hasSize(4) + .containsEntry("c1", new NewCookie.Builder("c1").value("1").build()) + .containsEntry("c2", new NewCookie.Builder("c2").value("11").build()) + .containsEntry("c3", new NewCookie.Builder("c3").value("3").build()) + .containsEntry("c4", new NewCookie.Builder("c4").value("4").build()); + + assertThat(config.getConfiguredPropertiesMap()).hasSize(4) + .containsEntry("prop1", "foo") + .containsEntry("prop2", "bar") + .containsEntry("prop3", "baz") + .containsEntry(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE); + + assertThat(config.getConfiguredExtensionsMap()).hasSize(3) + .containsEntry(Integer.class, Integer.class) + .containsEntry(Object.class, extension) + .containsEntry(VoidBodyReader.class, VoidBodyReader.class); + + assertThat(config.getConfiguredCacheControl()).isEqualTo(cacheControl); + assertThat(config.isDebugEnabled()).isTrue(); + + builder.invoke(); + assertThat(called).hasSize(1).containsOnly("called"); + } + + @Test + void testCustomMethods() { + TestClientRequestBuilder builder = new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .accept(MediaType.APPLICATION_JSON_TYPE) + .acceptLanguage(Locale.CANADA) + .cacheControl(cacheControl) + .debug(); + + TestRequestConfig config = builder.getConfig(); + assertThat(config.getConfiguredAccepts()).hasSize(1).containsOnly("application/json"); + assertThat(config.getConfiguredLanguages()).hasSize(1).containsOnly(Locale.CANADA.toString()); + assertThat(config.getConfiguredCacheControl()).isEqualTo(cacheControl); + assertThat(config.isDebugEnabled()).isTrue(); + } + + @Test + void testResponseTypeMappings() { + List calls = new ArrayList<>(); + new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .assertRequest(tracker -> { + calls.add("1"); + assertThat(tracker.getResultMappingClass()).isEqualTo(Void.class); + }) + .asVoid(); + + new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .assertRequest(tracker -> { + calls.add("2"); + assertThat(tracker.getResultMappingClass()).isEqualTo(Integer.class); + }) + .as(Integer.class); + + new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .assertRequest(tracker -> { + calls.add("3"); + assertThat(tracker.getResultMappingString()).isEqualTo("List"); + }) + .as(new GenericType>() {}); + + new TestClientRequestBuilder(new TargetMock(), "GET", null, null) + .assertRequest(tracker -> { + calls.add("4"); + assertThat(tracker.getResultMapping()).isNull(); + }) + .invoke(); + + assertThat(calls).containsExactly("1", "2", "3", "4"); + } + + @Test + void testToString() { + assertThat(new TestClientRequestBuilder(new TargetMock(), "GET", null, null).toString()) + .isEqualTo("Request builder: GET "); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfigTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfigTest.java new file mode 100644 index 000000000..7f840b7f1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/TestRequestConfigTest.java @@ -0,0 +1,382 @@ +package ru.vyarus.dropwizard.guice.test.client.builder; + +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.builder.track.RequestTracker; +import ru.vyarus.dropwizard.guice.test.client.builder.track.impl.mock.TargetMock; +import ru.vyarus.dropwizard.guice.test.client.util.SourceAwareValue; + +import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 03.10.2025 + */ +public class TestRequestConfigTest { + + final NewCookie cookie = new NewCookie.Builder("Test") + .value("tst") + .build(); + + final Function pathFunc = target -> target.path("foo"); + final Consumer reqFunc = req -> req.header("foo", "bar"); + final Object extension = new Object(); + final SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy"); + + @Test + void testEmptyConfig() { + // WHEN empty config + final TestRequestConfig config = new TestRequestConfig(null); + assertEmptyConfig(config); + } + + @Test + void testConfiguration() { + // WHEN filled config + final TestRequestConfig config = new TestRequestConfig(null) + .configurePath(pathFunc) + .configureRequest(reqFunc) + .property("prop", "true") + .register(Integer.class) + .register(extension) + .accept("text/plain") + .acceptLanguage("EN") + .acceptEncoding("gzip") + .cacheControl("max-age=604800, must-revalidate") + .queryParam("q1", "1") + .pathParam("p1", "2") + .matrixParam("m1", "1") + .header("Header", "value") + .cookie(cookie) + .formDateFormatter(formatter) + .formDateTimeFormatter(DateTimeFormatter.ISO_DATE) + .debug(true); + + assertFullConfig(config); + + // WHEN config inherit values + TestRequestConfig next = new TestRequestConfig(config); + assertFullConfig(next); + + // WHEN config reset + config.clear(); + assertEmptyConfig(config); + + // check no exception applying modifications (no way to asset correctness) + final RequestTracker tracker = new RequestTracker(); + next.applyRequestConfiguration(tracker.track()); + + assertThat(tracker.getPaths()).hasSize(1).contains("foo"); + assertThat(tracker.getProperties()).hasSize(1).containsEntry("prop", "true"); + assertThat(tracker.getExtensions()).hasSize(2) + .containsEntry(Integer.class, Integer.class) + .containsEntry(Object.class, extension); + assertThat(tracker.getCacheHeader()).isEqualTo("must-revalidate, max-age=604800"); + assertThat(tracker.getAcceptHeader()).containsOnly("text/plain"); + assertThat(tracker.getLanguageHeader()).containsOnly("EN"); + assertThat(tracker.getEncodingHeader()).containsOnly("gzip"); + assertThat(tracker.getQueryParams()).hasSize(1).containsEntry("q1", "1"); + assertThat(tracker.getPathParams()).hasSize(1).containsEntry("p1", "2"); + assertThat(tracker.getMatrixParams()).hasSize(1).containsEntry("m1", "1"); + assertThat(tracker.getHeaders()).hasSize(2) + .containsEntry("Header", "value") + .containsEntry("foo", "bar"); + assertThat(tracker.getCookies()).hasSize(1).containsEntry("Test", cookie); + + assertThat(tracker.getLog()).isEqualTo(""" + + Resolve template at r.v.d.g.t.c.builder.(TestRequestConfig.java:869) + (encodeSlashInPath=false encoded=true) + p1=2 + + Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + q1=1 + + Matrix param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82) + m1=1 + + Property at r.v.d.g.t.c.builder.(TestRequestConfig.java:879) + prop=true + + Register at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:59) + Integer (java.lang) \s + + Register at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:61) + Object (java.lang) \s + + Path at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:32) + foo + + Accept at r.v.d.g.t.c.builder.(TestRequestConfig.java:899) + [text/plain] + + Accept Language at r.v.d.g.t.c.builder.(TestRequestConfig.java:902) + [EN] + + Accept Encoding at r.v.d.g.t.c.builder.(TestRequestConfig.java:905) + [gzip] + + Header at r.v.d.g.t.c.builder.(TestRequestConfig.java:908) + Header=value + + Cookie at r.v.d.g.t.c.builder.(TestRequestConfig.java:911) + $Version=1;Test=tst + + Cache at r.v.d.g.t.c.builder.(TestRequestConfig.java:914) + must-revalidate, max-age=604800 + + Header at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:33) + foo=bar + + """); + } + + @Test + void testFlagsCorrectness() { + final TestRequestConfig config = new TestRequestConfig(null); + config.configurePath(pathFunc); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredPathModifiers()).hasSize(1).element(0).isEqualTo(pathFunc); + assertThat(config.getConfiguredPathModifiersSource()).hasSize(1) + .element(0).extracting(SourceAwareValue::get).isEqualTo(pathFunc); + + config.clear().configureRequest(reqFunc); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredRequestModifiers()).hasSize(1).element(0).isEqualTo(reqFunc); + assertThat(config.getConfiguredRequestModifiersSource()).hasSize(1) + .element(0).extracting(SourceAwareValue::get).isEqualTo(reqFunc); + + config.clear().property("prop", "true"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredProperties()).hasSize(1).containsOnly("prop"); + assertThat(config.getConfiguredPropertiesMap()).hasSize(1).containsEntry("prop", "true"); + assertThat(config.getConfiguredPropertiesSource()).hasSize(1).containsKey("prop"); + + config.clear().register(Integer.class); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredExtensions()).hasSize(1).containsOnly(Integer.class); + assertThat(config.getConfiguredExtensionsMap()).hasSize(1).containsEntry(Integer.class, Integer.class); + assertThat(config.getConfiguredExtensionsSource()).hasSize(1).containsKey(Integer.class); + + config.clear().accept("text/plain"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredAccepts()).hasSize(1).element(0).isEqualTo("text/plain"); + assertThat(config.getConfiguredAcceptsSource().get()).isEqualTo(new String[]{"text/plain"}); + + config.clear().acceptLanguage("EN"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredLanguages()).hasSize(1).element(0).isEqualTo("EN"); + assertThat(config.getConfiguredLanguagesSource().get()).isEqualTo(new String[]{"EN"}); + + config.clear().acceptEncoding("gzip"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredEncodings()).hasSize(1).element(0).isEqualTo("gzip"); + assertThat(config.getConfiguredEncodingsSource().get()).isEqualTo(new String[]{"gzip"}); + + config.clear().cacheControl("max-age=604800, must-revalidate"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredCacheControl().toString()).isEqualTo("must-revalidate, max-age=604800"); + assertThat(config.getConfiguredCacheControlSource().get().toString()).isEqualTo("must-revalidate, max-age=604800"); + + config.clear().queryParam("q1", "1"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredQueryParams()).hasSize(1).containsOnly("q1"); + assertThat(config.getConfiguredQueryParamsMap()).hasSize(1).containsEntry("q1", "1"); + assertThat(config.getConfiguredQueryParamsSource()).hasSize(1).containsKey("q1"); + + config.clear().pathParam("p1", "2"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredPathParams()).hasSize(1).containsOnly("p1"); + assertThat(config.getConfiguredPathParamsMap()).hasSize(1).containsEntry("p1", "2"); + assertThat(config.getConfiguredPathParamsSource()).hasSize(1).containsKey("p1"); + + config.clear().header("Header", "value"); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredHeaders()).hasSize(1).containsOnly("Header"); + assertThat(config.getConfiguredHeadersMap()).hasSize(1).containsEntry("Header", "value"); + assertThat(config.getConfiguredHeadersSource()).hasSize(1).containsKey("Header"); + + config.clear().cookie(cookie); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredCookies()).hasSize(1).containsOnly("Test"); + assertThat(config.getConfiguredCookiesMap()).hasSize(1).containsEntry("Test", cookie); + assertThat(config.getConfiguredCookiesSource()).hasSize(1).containsKey("Test"); + + config.clear().formDateFormatter(formatter); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredFormDateFormatter()).isEqualTo(formatter); + assertThat(config.getConfiguredFormDateFormatterSource()) + .extracting(SourceAwareValue::get).isEqualTo(formatter); + + config.clear().formDateTimeFormatter(DateTimeFormatter.ISO_DATE); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.getConfiguredFormDateTimeFormatter()).isEqualTo(DateTimeFormatter.ISO_DATE); + assertThat(config.getConfiguredFormDateTimeFormatterSource()) + .extracting(SourceAwareValue::get).isEqualTo(DateTimeFormatter.ISO_DATE); + + + assertThat(config.getConfiguredFormDateTimeFormatterSource().toString()) + .isEqualTo("Value from at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:222)"); + + // debug is not a part of defaults (but also copied) + config.clear().debug(true); + assertThat(config.hasConfiguration()).isFalse(); + assertThat(config.isDebugEnabled()).isTrue(); + } + + @Test + void testRequestAssertion() { + final TestRequestConfig config = new TestRequestConfig(null); + final List assertCalled = new ArrayList<>(); + config + .header("Header", "value") + .assertRequest(tracker -> assertCalled.add("called")); + + assertThat(config.isDebugEnabled()).isTrue(); + + config.applyRequestConfiguration(new TargetMock()).buildGet(); + assertThat(assertCalled).containsExactly("called"); + } + + private void assertEmptyConfig(TestRequestConfig config) { + assertThat(config.hasConfiguration()).isFalse(); + assertThat(config.printConfiguration()).isEqualTo(""" + + No configurations + """); + + assertThat(config.getConfiguredPathModifiers()).isEmpty(); + assertThat(config.getConfiguredRequestModifiers()).isEmpty(); + + assertThat(config.getConfiguredProperties()).isEmpty(); + assertThat(config.getConfiguredPropertiesMap()).isEmpty(); + + assertThat(config.getConfiguredExtensions()).isEmpty(); + assertThat(config.getConfiguredExtensionsMap()).isEmpty(); + + assertThat(config.getConfiguredCacheControl()).isNull(); + + assertThat(config.getConfiguredAccepts()).isEmpty(); + assertThat(config.getConfiguredLanguages()).isEmpty(); + assertThat(config.getConfiguredEncodings()).isEmpty(); + + assertThat(config.getConfiguredQueryParams()).isEmpty(); + assertThat(config.getConfiguredQueryParamsMap()).isEmpty(); + + assertThat(config.getConfiguredMatrixParams()).isEmpty(); + assertThat(config.getConfiguredMatrixParamsMap()).isEmpty(); + + assertThat(config.getConfiguredPathParams()).isEmpty(); + assertThat(config.getConfiguredPathParamsMap()).isEmpty(); + + assertThat(config.getConfiguredHeaders()).isEmpty(); + assertThat(config.getConfiguredHeadersMap()).isEmpty(); + + assertThat(config.getConfiguredCookies()).isEmpty(); + assertThat(config.getConfiguredCookiesMap()).isEmpty(); + + assertThat(config.getConfiguredFormDateFormatter()).isNull(); + assertThat(config.getConfiguredFormDateTimeFormatter()).isNull(); + + assertThat(config.isDebugEnabled()).isFalse(); + } + + private void assertFullConfig(TestRequestConfig config) { + config.printConfiguration(); + assertThat(config.hasConfiguration()).isTrue(); + assertThat(config.printConfiguration()).isEqualTo(""" + + Path params: + p1=2 at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:58) + + Query params: + q1=1 at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:57) + + Matrix params: + m1=1 at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:59) + + Headers: + Header=value at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:60) + + Cookies: + Test=tst;Version=1 at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:61) + + Properties: + prop=true at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:50) + + Extensions: + Integer at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:51) + Object at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:52) + + Accept: + text/plain at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:53) + + Language: + EN at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:54) + + Encoding: + gzip at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:55) + + Path modifiers: + at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:48) + + Request modifiers: + at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:49) + + Cache: + must-revalidate, max-age=604800 at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:56) + + Custom Date (java.util) formatter: + SimpleDateFormat at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:62) + + Custom Date (java.time) formatter: + DateTimeFormatter at r.v.d.g.t.c.builder.(TestRequestConfigTest.java:63) + """); + + assertThat(config.getConfiguredPathModifiers()).hasSize(1).element(0).isEqualTo(pathFunc); + assertThat(config.getConfiguredRequestModifiers()).hasSize(1).element(0).isEqualTo(reqFunc); + + assertThat(config.getConfiguredProperties()).containsOnly("prop"); + assertThat(config.getConfiguredPropertiesMap()).hasSize(1).containsEntry("prop", "true"); + + assertThat(config.getConfiguredExtensions()).containsOnly(Integer.class, Object.class); + assertThat(config.getConfiguredExtensionsMap()).hasSize(2) + .containsEntry(Integer.class, Integer.class) + .containsEntry(Object.class, extension); + + assertThat(RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class).toString(config.getConfiguredCacheControl())) + .isEqualTo("must-revalidate, max-age=604800"); + + assertThat(config.getConfiguredAccepts()).containsOnly("text/plain"); + assertThat(config.getConfiguredLanguages()).containsOnly("EN"); + assertThat(config.getConfiguredEncodings()).containsOnly("gzip"); + + assertThat(config.getConfiguredQueryParams()).containsOnly("q1"); + assertThat(config.getConfiguredQueryParamsMap()).hasSize(1).containsEntry("q1", "1"); + + assertThat(config.getConfiguredPathParams()).containsOnly("p1"); + assertThat(config.getConfiguredPathParamsMap()).hasSize(1).containsEntry("p1", "2"); + + assertThat(config.getConfiguredHeaders()).containsOnly("Header"); + assertThat(config.getConfiguredHeadersMap()).hasSize(1).containsEntry("Header", "value"); + + assertThat(config.getConfiguredCookies()).containsOnly("Test"); + assertThat(config.getConfiguredCookiesMap()).hasSize(1).containsEntry("Test", cookie); + + assertThat(config.getConfiguredFormDateFormatter()).isEqualTo(formatter); + assertThat(config.getConfiguredFormDateTimeFormatter()).isEqualTo(DateTimeFormatter.ISO_DATE); + + assertThat(config.isDebugEnabled()).isTrue(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelperTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelperTest.java new file mode 100644 index 000000000..7d8641e1a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/call/MultipartArgumentHelperTest.java @@ -0,0 +1,104 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.call; + +import org.assertj.core.api.Assertions; +import org.glassfish.jersey.media.multipart.ContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2025 + */ +public class MultipartArgumentHelperTest { + + @Test + void testContentDispositionBuilding() { + assertThat(MultipartArgumentHelper.createDispositionHeader("file", "text.xml")) + .isEqualTo("form-data; name=\"file\"; filename=\"text.xml\"; filename*=UTF-8''text.xml"); + + assertThat(MultipartArgumentHelper.createDispositionHeader("file", "файл_logback.xml")) + .isEqualTo("form-data; name=\"file\"; filename=\"_logback.xml\"; filename*=UTF-8''%D1%84%D0%B0%D0%B9%D0%BB_logback.xml"); + } + + @Test + void testDirectMethods() { + MultipartArgumentHelper helper = new MultipartArgumentHelper(); + + assertThat(helper.fromClasspath("/logback.xml")).isNotNull(); + assertThatThrownBy(() -> helper.fromClasspath("/unknown.txt")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Classpath resource '/unknown.txt' not found"); + + assertThat(helper.fromFile("src/test/resources/logback.xml")).isNotNull(); + assertThatThrownBy(() -> helper.fromFile("unknown.txt")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to read file 'unknown.txt' stream"); + + assertThat(helper.file("src/test/resources/logback.xml")).isNotNull(); + assertThatThrownBy(() -> helper.file("unknown.txt")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("'unknown.txt' does not exist or is a directory"); + assertThatThrownBy(() -> helper.file("src")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("'src' does not exist or is a directory"); + + assertThat(helper.disposition("file", new File("logback.xml"))).isNotNull() + .extracting(disp -> disp.getFileName(true)).isEqualTo("logback.xml"); + assertThat(helper.disposition("file", "logback.xml")).isNotNull() + .extracting(disp -> disp.getFileName(true)).isEqualTo("logback.xml"); + assertThat(helper.disposition("file", new File("файл_logback.xml"))).isNotNull() + .extracting(ContentDisposition::getFileName) + .isEqualTo("файл_logback.xml"); + + assertThat(helper.part("foo", "bar")) + .extracting(FormDataBodyPart::getName, FormDataBodyPart::getValue) + .containsExactly("foo", "bar"); + assertThat(helper.filePart("foo", new File("src/test/resources/logback.xml"))) + .extracting(FormDataBodyPart::getName, fileDataBodyPart -> fileDataBodyPart.getFileName().get()) + .containsExactly("foo", "logback.xml"); + assertThat(helper.filePart("foo", "src/test/resources/logback.xml")) + .extracting(FormDataBodyPart::getName, fileDataBodyPart -> fileDataBodyPart.getFileName().get()) + .containsExactly("foo", "logback.xml"); + + assertThat(helper.streamPart("foo", getClass().getResourceAsStream("/logback.xml"))) + .extracting(StreamDataBodyPart::getName, streamDataBodyPart -> streamDataBodyPart.getFileName().isPresent()) + .containsExactly("foo", false); + assertThat(helper.streamPart("foo", getClass().getResourceAsStream("/logback.xml"), "logback.xml")) + .extracting(StreamDataBodyPart::getName, streamDataBodyPart -> streamDataBodyPart.getFileName().get()) + .containsExactly("foo", "logback.xml"); + assertThat(helper.streamPart("foo", "/logback.xml")) + .extracting(StreamDataBodyPart::getName, streamDataBodyPart -> streamDataBodyPart.getFileName().get()) + .containsExactly("foo", "logback.xml"); + } + + @Test + void testBuilder() { + MultipartArgumentHelper helper = new MultipartArgumentHelper(); + + assertThat(helper.multipart().field("foo", "bar").build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(FormDataBodyPart.class); + + assertThat(helper.multipart().file("foo", new File("fl.txt")).build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(FileDataBodyPart.class); + assertThat(helper.multipart().file("foo", new File("fl.txt"), new File("fl2.txt")).build().getBodyParts()).hasSize(2); + + assertThat(helper.multipart().file("foo", "src/test/resources/logback.xml").build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(FileDataBodyPart.class); + + assertThat(helper.multipart().stream("foo", "/logback.xml").build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(StreamDataBodyPart.class); + + assertThat(helper.multipart().stream("foo", helper.fromClasspath("/logback.xml")).build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(StreamDataBodyPart.class); + assertThat(helper.multipart().stream("foo", helper.fromClasspath("/logback.xml"), "logback.xml").build().getBodyParts()).hasSize(1) + .element(0).isInstanceOf(StreamDataBodyPart.class) + .extracting(bodyPart -> bodyPart.getContentDisposition().getFileName()).isEqualTo("logback.xml"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/ResponseTypeTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/ResponseTypeTest.java new file mode 100644 index 000000000..a573c8de5 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/ResponseTypeTest.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import jakarta.ws.rs.core.GenericType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +/** + * @author Vyacheslav Rusakov + * @since 06.10.2025 + */ +public class ResponseTypeTest { + + @Test + void testResponseTypeMapping() { + + RequestTracker tracker = new RequestTracker(); + tracker.track() + .request().get(new GenericType>>(){}); + + Assertions.assertThat(tracker.getResultMappingClass()).isEqualTo(List.class); + Assertions.assertThat(tracker.getResultMappingString()).isEqualTo("List>"); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackMockTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackMockTest.java new file mode 100644 index 000000000..b142c5e83 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackMockTest.java @@ -0,0 +1,406 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.glassfish.jersey.client.JerseyCompletionStageRxInvoker; +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 06.10.2025 + */ +public class TrackMockTest { + + @Test + void testTargetMethods() { + + final RequestTracker tracker = new RequestTracker(); + final WebTarget target = tracker.track(); + target.path("some/{name}") + .resolveTemplate("name", "nm") + .resolveTemplate("name2", "vv//vv", true) + .resolveTemplateFromEncoded("name3", "3") + .resolveTemplates(ImmutableMap.of("name4", "4")) + .resolveTemplates(ImmutableMap.of("name5", "5"), true) + .resolveTemplatesFromEncoded(ImmutableMap.of("name6", "6")) + .matrixParam("mx", "1") + .matrixParam("mx2", "1", "2") + .queryParam("qq", "qq") + .queryParam("qq2", "1", "2") + .property("foo", "bar") + .register(Ext1.class) + .register(Ext2.class, 10) + .register(Ext3.class, Contr1.class, Contr2.class) + .register(Ext4.class, ImmutableMap.of(Contr1.class, 11)) + .register(new Ext5()) + .register(new Ext6(), 10) + .register(new Ext7(), Contr1.class, Contr2.class) + .register(new Ext8(), ImmutableMap.of(Contr1.class, 11)); + + assertThat(tracker.getPaths()).containsOnly("some/{name}"); + assertThat(tracker.getPathParams()).hasSize(6) + .containsEntry("name", "nm") + .containsEntry("name2", "vv//vv") + .containsEntry("name3", "3") + .containsEntry("name4", "4") + .containsEntry("name5", "5") + .containsEntry("name6", "6"); + assertThat(tracker.getRawData().getPathParams().get(1).get()) + .extracting("name", "value", "encodeSlashInPath", "encoded") + .containsExactly("name2", "vv//vv", true, false); + assertThat(tracker.getMatrixParams()) + .hasSize(2) + .containsEntry("mx", "1") + .containsEntry("mx2", new Object[]{"1", "2"}); + assertThat(tracker.getQueryParams()) + .hasSize(2) + .containsEntry("qq", "qq") + .containsEntry("qq2", new Object[]{"1", "2"}); + assertThat(tracker.getProperties()).hasSize(1) + .containsEntry("foo", "bar"); + assertThat(tracker.getExtensions()).hasSize(8) + .containsKeys(Ext1.class, Ext2.class, Ext3.class, Ext4.class, Ext5.class, Ext6.class, Ext7.class, Ext8.class); + assertThat(tracker.getRawData().getExtensions().get(Ext3.class).get()) + .extracting("type", "value", "contracts") + .containsExactly(Ext3.class, Ext3.class, ImmutableMap.of(Contr1.class, -1, Contr2.class, -1)); + assertThat(tracker.getUrl()).isNull(); + + assertThat(tracker.getLog()).isEqualTo(""" + + Path at r.v.d.g.t.c.b.track.(TrackMockTest.java:32) + some/{name} + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:33) + (encodeSlashInPath=false encoded=false) + name=nm + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:34) + (encodeSlashInPath=true encoded=false) + name2=vv//vv + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:35) + (encodeSlashInPath=false encoded=true) + name3=3 + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:36) + (encodeSlashInPath=false encoded=false) + name4=4 + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:37) + (encodeSlashInPath=true encoded=false) + name5=5 + + Resolve template at r.v.d.g.t.c.b.track.(TrackMockTest.java:38) + (encodeSlashInPath=false encoded=true) + name6=6 + + Matrix param at r.v.d.g.t.c.b.track.(TrackMockTest.java:39) + mx=1 + + Matrix param at r.v.d.g.t.c.b.track.(TrackMockTest.java:40) + mx2=[1, 2] + + Query param at r.v.d.g.t.c.b.track.(TrackMockTest.java:41) + qq=qq + + Query param at r.v.d.g.t.c.b.track.(TrackMockTest.java:42) + qq2=[1, 2] + + Property at r.v.d.g.t.c.b.track.(TrackMockTest.java:43) + foo=bar + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:44) + Ext1 (r.v.d.g.t.c.b.t.TrackMockTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:45) + Ext2 (r.v.d.g.t.c.b.t.TrackMockTest)\s + priority=10 + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:46) + Ext3 (r.v.d.g.t.c.b.t.TrackMockTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackMockTest)\s + Contr2 (r.v.d.g.t.c.b.t.TrackMockTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:47) + Ext4 (r.v.d.g.t.c.b.t.TrackMockTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackMockTest) =11 + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:48) + Ext5 (r.v.d.g.t.c.b.t.TrackMockTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:49) + Ext6 (r.v.d.g.t.c.b.t.TrackMockTest)\s + priority=10 + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:50) + Ext7 (r.v.d.g.t.c.b.t.TrackMockTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackMockTest)\s + Contr2 (r.v.d.g.t.c.b.t.TrackMockTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackMockTest.java:51) + Ext8 (r.v.d.g.t.c.b.t.TrackMockTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackMockTest) =11 + + """); + + + assertThat(target.getUriBuilder()).isNotNull(); + assertThat(target.getConfiguration()).isNull(); + assertThat(target.getUri().toString()).isEqualTo("some/nm"); + + assertThat(target.request()).isNotNull(); + + assertThat(target.request(MediaType.TEXT_PLAIN)).isNotNull(); + assertThat(tracker.getAcceptHeader()).containsOnly(MediaType.TEXT_PLAIN); + + assertThat(target.request(MediaType.APPLICATION_JSON_TYPE)).isNotNull(); + assertThat(tracker.getAcceptHeader()).containsOnly(MediaType.APPLICATION_JSON); + + assertThat(tracker.getUrl()).isEqualTo("some/nm"); + } + + @Test + void testBuilderMethods() { + final RequestTracker tracker = new RequestTracker(); + final Invocation.Builder builder = tracker.track().path("some/nm").request(); + + builder.property("foo", "bar") + .accept(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_JSON_TYPE) + .acceptLanguage("EN") + .acceptLanguage(Locale.CANADA) + .acceptEncoding("gzip") + .cookie("c1", "1") + .cookie(new NewCookie.Builder("c2").value("2").build()) + .cacheControl(RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString("max-age=604800, must-revalidate")) + .header("h1", "1") + .headers(new MultivaluedHashMap<>(ImmutableMap.of("h2", "2", "h3", "3"))); + + assertThat(tracker.getProperties()).hasSize(1) + .containsEntry("foo", "bar"); + assertThat(tracker.getAcceptHeader()).hasSize(1) + .containsOnly(MediaType.APPLICATION_JSON_TYPE.toString()); + assertThat(tracker.getLanguageHeader()).hasSize(1) + .containsOnly(Locale.CANADA.toString()); + assertThat(tracker.getEncodingHeader()).hasSize(1) + .containsOnly("gzip"); + assertThat(tracker.getCookies()).hasSize(2) + .containsEntry("c1", new NewCookie.Builder("c1").value("1").build()) + .containsEntry("c2", new NewCookie.Builder("c2").value("2").build()); + assertThat(tracker.getCacheHeader()).isEqualTo("must-revalidate, max-age=604800"); + assertThat(tracker.getHeaders()).hasSize(3) + .containsEntry("h1", "1") + .containsEntry("h2", "2") + .containsEntry("h3", "3"); + + assertThat(tracker.getLog()).isEqualTo(""" + + Path at r.v.d.g.t.c.b.track.(TrackMockTest.java:182) + some/nm + + Property at r.v.d.g.t.c.b.track.(TrackMockTest.java:184) + foo=bar + + Accept at r.v.d.g.t.c.b.track.(TrackMockTest.java:185) + [text/plain] + + Accept at r.v.d.g.t.c.b.track.(TrackMockTest.java:186) + [application/json] + + Accept Language at r.v.d.g.t.c.b.track.(TrackMockTest.java:187) + [EN] + + Accept Language at r.v.d.g.t.c.b.track.(TrackMockTest.java:188) + [en_CA] + + Accept Encoding at r.v.d.g.t.c.b.track.(TrackMockTest.java:189) + [gzip] + + Cookie at r.v.d.g.t.c.b.track.(TrackMockTest.java:190) + $Version=1;c1=1 + + Cookie at r.v.d.g.t.c.b.track.(TrackMockTest.java:191) + $Version=1;c2=2 + + Cache at r.v.d.g.t.c.b.track.(TrackMockTest.java:192) + must-revalidate, max-age=604800 + + Header at r.v.d.g.t.c.b.track.(TrackMockTest.java:194) + h1=1 + + Headers at r.v.d.g.t.c.b.track.(TrackMockTest.java:195) + h2=[2] + h3=[3] + + """); + + builder.build("GET"); + verifyMethod(tracker, "GET", null, null); + + + builder.buildGet(); + verifyMethod(tracker, "GET", null, null); + + assertThat(builder.get()).isNull(); + verifyMethod(tracker, "GET", null, null); + + assertThat(builder.get(Integer.class)).isNull(); + verifyMethod(tracker, "GET", null, Integer.class); + + assertThat(builder.get(new GenericType() {})).isNull(); + verifyMethod(tracker, "GET", null, Integer.class); + + + + builder.build("POST", Entity.text("test")); + verifyMethod(tracker, "POST", Entity.text("test"), null); + + builder.buildPost(Entity.text("test")); + verifyMethod(tracker, "POST", Entity.text("test"), null); + + assertThat(builder.post(Entity.text("test"))).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), null); + + assertThat(builder.post(Entity.text("test"), Integer.class)).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), Integer.class); + + assertThat(builder.post(Entity.text("test"), new GenericType() {})).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), Integer.class); + + + builder.buildDelete(); + verifyMethod(tracker, "DELETE", null, null); + + assertThat(builder.delete()).isNull(); + verifyMethod(tracker, "DELETE", null, null); + + assertThat(builder.delete(Integer.class)).isNull(); + verifyMethod(tracker, "DELETE", null, Integer.class); + + assertThat(builder.delete(new GenericType() {})).isNull(); + verifyMethod(tracker, "DELETE", null, Integer.class); + + + + builder.buildPut(Entity.text("test")); + verifyMethod(tracker, "PUT", Entity.text("test"), null); + + assertThat(builder.put(Entity.text("test"))).isNull(); + verifyMethod(tracker, "PUT", Entity.text("test"), null); + + assertThat(builder.put(Entity.text("test"), Integer.class)).isNull(); + verifyMethod(tracker, "PUT", Entity.text("test"), Integer.class); + + assertThat(builder.put(Entity.text("test"), new GenericType() {})).isNull(); + verifyMethod(tracker, "PUT", Entity.text("test"), Integer.class); + + + assertThat(builder.head()).isNull(); + verifyMethod(tracker, "HEAD", null, null); + + + assertThat(builder.options()).isNull(); + verifyMethod(tracker, "OPTIONS", null, null); + + assertThat(builder.options(Integer.class)).isNull(); + verifyMethod(tracker, "OPTIONS", null, Integer.class); + + assertThat(builder.options(new GenericType() {})).isNull(); + verifyMethod(tracker, "OPTIONS", null, Integer.class); + + + assertThat(builder.trace()).isNull(); + verifyMethod(tracker, "TRACE", null, null); + + assertThat(builder.trace(Integer.class)).isNull(); + verifyMethod(tracker, "TRACE", null, Integer.class); + + assertThat(builder.trace(new GenericType() {})).isNull(); + verifyMethod(tracker, "TRACE", null, Integer.class); + + + assertThat(builder.method("TRACE")).isNull(); + verifyMethod(tracker, "TRACE", null, null); + + assertThat(builder.method("GET", Integer.class)).isNull(); + verifyMethod(tracker, "GET", null, Integer.class); + + assertThat(builder.method("GET", new GenericType() {})).isNull(); + verifyMethod(tracker, "GET", null, Integer.class); + + assertThat(builder.method("POST", Entity.text("test"))).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), null); + + assertThat(builder.method("POST", Entity.text("test"), Integer.class)).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), Integer.class); + + assertThat(builder.method("POST", Entity.text("test"), new GenericType() {})).isNull(); + verifyMethod(tracker, "POST", Entity.text("test"), Integer.class); + + + assertThat(builder.async()).isNull(); + assertThat(builder.rx()).isNull(); + assertThat(builder.rx(JerseyCompletionStageRxInvoker.class)).isNull(); + } + + @Test + void testTrackerLookup() { + RequestTracker tracker = new RequestTracker(); + final WebTarget target = tracker.track(); + + assertThat(RequestTracker.lookupTracker(target).get()).isEqualTo(tracker); + assertThat(RequestTracker.lookupTracker(target.request()).get()).isEqualTo(tracker); + } + + private void verifyMethod(RequestTracker tracker, String method, Entity entity, Class result) { + assertThat(tracker.getHttpMethod()).isEqualTo(method); + if (entity == null) { + assertThat(tracker.getEntity()).isNull(); + } else { + assertThat(tracker.getEntity()).isEqualTo(entity); + } + if (result != null) { + assertThat(tracker.getResultMappingClass()).isEqualTo(result); + } else { + assertThat(tracker.getResultMapping()).isNull(); + } + } + + public static class Ext1 {} + + public static class Ext2 {} + + public static class Ext3 {} + + public static class Ext4 {} + + public static class Ext5 {} + + public static class Ext6 {} + + public static class Ext7 {} + + public static class Ext8 {} + + public static class Contr1 {} + + public static class Contr2 {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackRealTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackRealTest.java new file mode 100644 index 000000000..30bcb8e6d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/TrackRealTest.java @@ -0,0 +1,457 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.NotAllowedException; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.glassfish.jersey.client.JerseyCompletionStageRxInvoker; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 06.10.2025 + */ +@TestDropwizardApp(value = TrackRealTest.App.class, randomPorts = true) +public class TrackRealTest { + + @Test + void testTargetMethods(ClientSupport client) { + + final RequestTracker tracker = new RequestTracker(); + final WebTarget target = tracker.track(client.target("/")); + target.path("root") + .resolveTemplate("name", "nm") + .resolveTemplate("name2", "vv//vv", true) + .resolveTemplateFromEncoded("name3", "3") + .resolveTemplates(ImmutableMap.of("name4", "4")) + .resolveTemplates(ImmutableMap.of("name5", "5"), true) + .resolveTemplatesFromEncoded(ImmutableMap.of("name6", "6")) + .matrixParam("mx", "1") + .matrixParam("mx2", "1", "2") + .queryParam("qq", "qq") + .queryParam("qq2", "1", "2") + .property("foo", "bar") + .register(Ext1.class) + .register(Ext2.class, 10) + .register(Ext3.class, Contr1.class, Contr2.class) + .register(Ext4.class, ImmutableMap.of(Contr1.class, 11)) + .register(new Ext5()) + .register(new Ext6(), 10) + .register(new Ext7(), Contr1.class, Contr2.class) + .register(new Ext8(), ImmutableMap.of(Contr1.class, 11)); + + assertThat(tracker.getPaths()).containsOnly("root"); + assertThat(tracker.getPathParams()).hasSize(6) + .containsEntry("name", "nm") + .containsEntry("name2", "vv//vv") + .containsEntry("name3", "3") + .containsEntry("name4", "4") + .containsEntry("name5", "5") + .containsEntry("name6", "6"); + assertThat(tracker.getRawData().getPathParams().get(1).get()) + .extracting("name", "value", "encodeSlashInPath", "encoded") + .containsExactly("name2", "vv//vv", true, false); + assertThat(tracker.getMatrixParams()) + .hasSize(2) + .containsEntry("mx", "1") + .containsEntry("mx2", new Object[]{"1", "2"}); + assertThat(tracker.getQueryParams()) + .hasSize(2) + .containsEntry("qq", "qq") + .containsEntry("qq2", new Object[]{"1", "2"}); + assertThat(tracker.getProperties()).hasSize(1) + .containsEntry("foo", "bar"); + assertThat(tracker.getExtensions()).hasSize(8) + .containsKeys(Ext1.class, Ext2.class, Ext3.class, Ext4.class, Ext5.class, Ext6.class, Ext7.class, Ext8.class); + assertThat(tracker.getRawData().getExtensions().get(Ext3.class).get()) + .extracting("type", "value", "contracts") + .containsExactly(Ext3.class, Ext3.class, ImmutableMap.of(Contr1.class, -1, Contr2.class, -1)); + assertThat(tracker.getUrl()).isNull(); + + assertThat(tracker.getLog()).isEqualTo(""" + + Path at r.v.d.g.t.c.b.track.(TrackRealTest.java:46) + root + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:47) + (encodeSlashInPath=false encoded=false) + name=nm + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:48) + (encodeSlashInPath=true encoded=false) + name2=vv//vv + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:49) + (encodeSlashInPath=false encoded=true) + name3=3 + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:50) + (encodeSlashInPath=false encoded=false) + name4=4 + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:51) + (encodeSlashInPath=true encoded=false) + name5=5 + + Resolve template at r.v.d.g.t.c.b.track.(TrackRealTest.java:52) + (encodeSlashInPath=false encoded=true) + name6=6 + + Matrix param at r.v.d.g.t.c.b.track.(TrackRealTest.java:53) + mx=1 + + Matrix param at r.v.d.g.t.c.b.track.(TrackRealTest.java:54) + mx2=[1, 2] + + Query param at r.v.d.g.t.c.b.track.(TrackRealTest.java:55) + qq=qq + + Query param at r.v.d.g.t.c.b.track.(TrackRealTest.java:56) + qq2=[1, 2] + + Property at r.v.d.g.t.c.b.track.(TrackRealTest.java:57) + foo=bar + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:58) + Ext1 (r.v.d.g.t.c.b.t.TrackRealTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:59) + Ext2 (r.v.d.g.t.c.b.t.TrackRealTest)\s + priority=10 + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:60) + Ext3 (r.v.d.g.t.c.b.t.TrackRealTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackRealTest)\s + Contr2 (r.v.d.g.t.c.b.t.TrackRealTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:61) + Ext4 (r.v.d.g.t.c.b.t.TrackRealTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackRealTest) =11 + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:62) + Ext5 (r.v.d.g.t.c.b.t.TrackRealTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:63) + Ext6 (r.v.d.g.t.c.b.t.TrackRealTest)\s + priority=10 + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:64) + Ext7 (r.v.d.g.t.c.b.t.TrackRealTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackRealTest)\s + Contr2 (r.v.d.g.t.c.b.t.TrackRealTest)\s + + Register at r.v.d.g.t.c.b.track.(TrackRealTest.java:65) + Ext8 (r.v.d.g.t.c.b.t.TrackRealTest)\s + contracts= + Contr1 (r.v.d.g.t.c.b.t.TrackRealTest) =11 + + """); + + + assertThat(target.getUriBuilder()).isNotNull(); + assertThat(target.getConfiguration()).isNotNull(); + assertThat(target.getUri().toString()).isEqualTo("http://localhost:"+client.getPort() + "/root;mx=1;mx2=1;mx2=2?qq=qq&qq2=1&qq2=2"); + + assertThat(target.request()).isNotNull(); + + assertThat(target.request(MediaType.TEXT_PLAIN)).isNotNull(); + assertThat(tracker.getAcceptHeader()).containsOnly(MediaType.TEXT_PLAIN); + + assertThat(target.request(MediaType.APPLICATION_JSON_TYPE)).isNotNull(); + assertThat(tracker.getAcceptHeader()).containsOnly(MediaType.APPLICATION_JSON); + + assertThat(tracker.getUrl()).isEqualTo("http://localhost:"+client.getPort() + "/root;mx=1;mx2=1;mx2=2?qq=qq&qq2=1&qq2=2"); + } + + @Test + void testBuilderMethods(ClientSupport client) { + final RequestTracker tracker = new RequestTracker(); + final Invocation.Builder builder = tracker.track(client.targetRest("root")).request(); + + builder.property("foo", "bar") + .accept(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_JSON_TYPE) + .acceptLanguage("EN") + .acceptLanguage(Locale.CANADA) + .acceptEncoding("gzip") + .cookie("c1", "1") + .cookie(new NewCookie.Builder("c2").value("2").build()) + .cacheControl(RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString("max-age=604800, must-revalidate")) + .header("h1", "1") + .headers(new MultivaluedHashMap<>(ImmutableMap.of("h2", "2", "h3", "3"))); + + assertThat(tracker.getProperties()).hasSize(1) + .containsEntry("foo", "bar"); + assertThat(tracker.getAcceptHeader()).hasSize(1) + .containsOnly(MediaType.APPLICATION_JSON_TYPE.toString()); + assertThat(tracker.getLanguageHeader()).hasSize(1) + .containsOnly(Locale.CANADA.toString()); + assertThat(tracker.getEncodingHeader()).hasSize(1) + .containsOnly("gzip"); + assertThat(tracker.getCookies()).hasSize(2) + .containsEntry("c1", new NewCookie.Builder("c1").value("1").build()) + .containsEntry("c2", new NewCookie.Builder("c2").value("2").build()); + assertThat(tracker.getCacheHeader()).isEqualTo("must-revalidate, max-age=604800"); + assertThat(tracker.getHeaders()).hasSize(3) + .containsEntry("h1", "1") + .containsEntry("h2", "2") + .containsEntry("h3", "3"); + + assertThat(tracker.getLog()).isEqualTo(""" + + Property at r.v.d.g.t.c.b.track.(TrackRealTest.java:198) + foo=bar + + Accept at r.v.d.g.t.c.b.track.(TrackRealTest.java:199) + [text/plain] + + Accept at r.v.d.g.t.c.b.track.(TrackRealTest.java:200) + [application/json] + + Accept Language at r.v.d.g.t.c.b.track.(TrackRealTest.java:201) + [EN] + + Accept Language at r.v.d.g.t.c.b.track.(TrackRealTest.java:202) + [en_CA] + + Accept Encoding at r.v.d.g.t.c.b.track.(TrackRealTest.java:203) + [gzip] + + Cookie at r.v.d.g.t.c.b.track.(TrackRealTest.java:204) + $Version=1;c1=1 + + Cookie at r.v.d.g.t.c.b.track.(TrackRealTest.java:205) + $Version=1;c2=2 + + Cache at r.v.d.g.t.c.b.track.(TrackRealTest.java:206) + must-revalidate, max-age=604800 + + Header at r.v.d.g.t.c.b.track.(TrackRealTest.java:208) + h1=1 + + Headers at r.v.d.g.t.c.b.track.(TrackRealTest.java:209) + h2=[2] + h3=[3] + + """); + + builder.build("GET"); + verifyMethod(tracker, "GET", null); + + + builder.buildGet(); + verifyMethod(tracker, "GET", null); + + assertThat(builder.get()).isNotNull(); + verifyMethod(tracker, "GET", null); + + assertThat(builder.get(Integer.class)).isNotNull(); + verifyMethod(tracker, "GET", null); + + assertThat(builder.get(new GenericType() {})).isNotNull(); + verifyMethod(tracker, "GET", null); + + + builder.buildDelete(); + verifyMethod(tracker, "DELETE", null); + + assertThat(builder.delete()).isNotNull(); + verifyMethod(tracker, "DELETE", null); + + assertThat(builder.delete(Integer.class)).isNotNull(); + verifyMethod(tracker, "DELETE", null); + + assertThat(builder.delete(new GenericType() {})).isNotNull(); + verifyMethod(tracker, "DELETE", null); + + + assertThat(builder.head()).isNotNull(); + verifyMethod(tracker, "HEAD", null); + + + assertThat(builder.options()).isNotNull(); + verifyMethod(tracker, "OPTIONS", null); + + assertThat(builder.options(Integer.class)).isNotNull(); + verifyMethod(tracker, "OPTIONS", null); + + assertThat(builder.options(new GenericType() {})).isNotNull(); + verifyMethod(tracker, "OPTIONS", null); + + + assertThat(builder.trace()).isNotNull(); + verifyMethod(tracker, "TRACE", null); + + assertThatThrownBy(() -> builder.trace(Integer.class)); + verifyMethod(tracker, "TRACE", null); + + assertThatThrownBy(() ->builder.trace(new GenericType() {})); + verifyMethod(tracker, "TRACE", null); + + + assertThat(builder.method("TRACE")).isNotNull(); + verifyMethod(tracker, "TRACE", null); + + assertThat(builder.method("GET", Integer.class)).isNotNull(); + verifyMethod(tracker, "GET", null); + + assertThat(builder.method("GET", new GenericType() {})).isNotNull(); + verifyMethod(tracker, "GET", null); + + + + + builder.build("POST", Entity.text("test")); + verifyMethod(tracker, "POST", Entity.text("test")); + + builder.buildPost(Entity.text("test")); + verifyMethod(tracker, "POST", Entity.text("test")); + + assertThat(builder.post(Entity.text("test"))).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + assertThat(builder.post(Entity.text("test"), Integer.class)).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + assertThat(builder.post(Entity.text("test"), new GenericType() {})).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + + + builder.buildPut(Entity.text("test")); + verifyMethod(tracker, "PUT", Entity.text("test")); + + assertThat(builder.put(Entity.text("test"))).isNotNull(); + verifyMethod(tracker, "PUT", Entity.text("test")); + + assertThat(builder.put(Entity.text("test"), Integer.class)).isNotNull(); + verifyMethod(tracker, "PUT", Entity.text("test")); + + assertThat(builder.put(Entity.text("test"), new GenericType() {})).isNotNull(); + verifyMethod(tracker, "PUT", Entity.text("test")); + + + assertThat(builder.method("POST", Entity.text("test"))).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + assertThat(builder.method("POST", Entity.text("test"), Integer.class)).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + assertThat(builder.method("POST", Entity.text("test"), new GenericType() {})).isNotNull(); + verifyMethod(tracker, "POST", Entity.text("test")); + + + assertThat(builder.async()).isNotNull(); + assertThat(builder.rx()).isNotNull(); + assertThat(builder.rx(JerseyCompletionStageRxInvoker.class)).isNotNull(); + } + + @Test + void testTrackerLookup(ClientSupport client) { + RequestTracker tracker = new RequestTracker(); + final WebTarget target = tracker.track(client.target("/")); + + assertThat(RequestTracker.lookupTracker(target).get()).isEqualTo(tracker); + assertThat(RequestTracker.lookupTracker(target.request()).get()).isEqualTo(tracker); + } + + private void verifyMethod(RequestTracker tracker, String method, Entity entity) { + assertThat(tracker.getHttpMethod()).isEqualTo(method); + if (entity == null) { + assertThat(tracker.getEntity()).isNull(); + } else { + assertThat(tracker.getEntity()).isEqualTo(entity); + } + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Resource.class) + .build(); + } + } + + @Path("/root") + public static class Resource { + + @GET + @Path("/") + public int get() { + return 1; + } + + @POST + @Path("/") + public int post(String text) { + return 1; + } + + @PUT + @Path("/") + public int put(String text) { + return 1; + } + + @DELETE + @Path("/") + public int delete() { + return 1; + } + + @OPTIONS + @Path("/") + public int post() { + return 1; + } + } + + public static class Ext1 {} + + public static class Ext2 {} + + public static class Ext3 {} + + public static class Ext4 {} + + public static class Ext5 {} + + public static class Ext6 {} + + public static class Ext7 {} + + public static class Ext8 {} + + public static class Contr1 {} + + public static class Contr2 {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/UriBuilderTrackTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/UriBuilderTrackTest.java new file mode 100644 index 000000000..e7c73015c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/track/UriBuilderTrackTest.java @@ -0,0 +1,94 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.track; + +import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.core.UriBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 06.10.2025 + */ +public class UriBuilderTrackTest { + + @Test + void testUriBuilderTrack() throws Exception { + + RequestTracker tracker = new RequestTracker(); + final UriBuilder builder = tracker.track().getUriBuilder(); + builder.uri(URI.create("https://google.com")) + .uri("http://google.com") + .scheme("http") + .schemeSpecificPart("/something") + .userInfo("a:A") + .host("localhost") + .port(8080) + .replacePath("/other") + .path("api") + .path(TrackRealTest.Resource.class) + .path(TrackRealTest.Resource.class, "get") + .path(TrackRealTest.Resource.class.getMethod("get")) + .segment("segment", "segment2") + .replaceMatrix("a=2;b=3;g") + .matrixParam("c", 4) + .replaceMatrixParam("c", 5) + .replaceQuery("q1=1&q2=2") + .queryParam("q2", 3) + .replaceQueryParam("q3", 4) + .fragment("fragment") + .resolveTemplate("p1", "1") + .resolveTemplate("p2", "2//3", true) + .resolveTemplateFromEncoded("p3", "3") + .resolveTemplates(ImmutableMap.of("p4", "4", "p5", "5")) + .resolveTemplates(ImmutableMap.of("p6", "6//7"), true) + .resolveTemplatesFromEncoded(ImmutableMap.of("p7", "7")); + + assertThat(tracker.getPaths()).containsOnly( + "https://google.com", + "http://google.com", + "http", + "/something", + "a:A", + "localhost", + "8080", + "/other", + "api", + "/root", + "/", + "/", + "segment/segment2", + "fragment"); + assertThat(tracker.getPathParams()).hasSize(7) + .containsEntry("p1", "1") + .containsEntry("p2", "2//3") + .containsEntry("p3", "3") + .containsEntry("p4", "4") + .containsEntry("p5", "5") + .containsEntry("p6", "6//7") + .containsEntry("p7", "7"); + assertThat(tracker.getMatrixParams()).hasSize(4) + .containsEntry("a", "2") + .containsEntry("b", "3") + .containsEntry("c", 5) + .containsEntry("g", null); + assertThat(tracker.getQueryParams()).hasSize(3) + .containsEntry("q1", "1") + .containsEntry("q2", 3) + .containsEntry("q3", 4); + + assertThatThrownBy(builder::clone).isInstanceOf(UnsupportedOperationException.class); + assertThat(builder.buildFromMap(ImmutableMap.of("1", "2"))).isNotNull(); + assertThat(builder.buildFromMap(ImmutableMap.of("1", "2"), true)).isNotNull(); + assertThat(builder.buildFromEncodedMap(ImmutableMap.of("1", "2"))).isNotNull(); + assertThat(builder.build("1", "2")).isNotNull(); + assertThat(builder.build(new Object[]{"1", "2"}, true)).isNotNull(); + assertThat(builder.buildFromEncoded("1", "2")).isNotNull(); + assertThat(builder.toTemplate()).isNotNull(); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/DispositionParseTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/DispositionParseTest.java new file mode 100644 index 000000000..dac137093 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/DispositionParseTest.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.MultipartSupport; +import ru.vyarus.dropwizard.guice.test.client.util.FileDownloadUtil; + +/** + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +public class DispositionParseTest { + + @Test + void testManualDispositionParse() { + check("attachment; filename=document.pdf", "document.pdf"); + check("attachment; filename=\"document.pdf\"", "document.pdf"); + check("attachment; filename*=UTF-8''%e2%82%ac%20rates", "€ rates"); + check("attachment; filename*=\"UTF-8''%e2%82%ac%20rates\"", "€ rates"); + check("attachment; filename=doc.doc; filename*=UTF-8''%e2%82%ac%20rates", "€ rates"); + } + + private void check(String header, String result) { + String directParse = MultipartSupport.readFilename(header); + Assertions.assertThat(directParse).as("Incorrect reference result").isEqualTo(result); + Assertions.assertThat(directParse).as("incorrect manual parsing").isEqualTo(FileDownloadUtil.parseFileName(header)); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/FormParamsSupportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/FormParamsSupportTest.java new file mode 100644 index 000000000..6921970ea --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/builder/util/FormParamsSupportTest.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.test.client.builder.util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.builder.util.conf.FormParamsSupport; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +public class FormParamsSupportTest { + + @Test + void testParametersToString() throws Exception { + verify(null, ""); + verify("", ""); + verify(" ", " "); + verify("abc", "abc"); + verify("", ""); + verify(1, "1"); + verify(Arrays.asList(1,2,3), "1,2,3"); + verify(new Integer[]{1,2,3}, "1,2,3"); + verify(FormParamsSupport.DEFAULT_DATE_FORMAT.parse("2011-11-01T11:01:00"), "2011-11-01T11:01:00.000+00:00"); + verify(FormParamsSupport.DEFAULT_DATE_TIME_FORMAT.parse("2011-11-01T11:01:00Z"), "2011-11-01T11:01:00Z"); + } + + private void verify(Object value, String result) { + Assertions.assertThat(FormParamsSupport.parameterToString(value)).as(result + " verification").isEqualTo(result); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ClientApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ClientApp.java new file mode 100644 index 000000000..5e1cff033 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ClientApp.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; + +/** + * @author Vyacheslav Rusakov + * @since 07.10.2025 + */ +public class ClientApp extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions( + Resource.class, + SuccFailRedirectResource.class, + FileResource.class, + FormResource.class, + FormBeanResource.class, + ErrorsResource.class, + MatrixResource.class, + PrimitivesResource.class) + .build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ErrorsResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ErrorsResource.java new file mode 100644 index 000000000..605a54cdd --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/ErrorsResource.java @@ -0,0 +1,80 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2025 + */ +@Path("/errors") +public class ErrorsResource { + + @Path("/bad") + @GET + public Response bad() { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + @Path("/unauth") + @GET + public Response unauth() { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + @Path("/forbid") + @GET + public Response forbid() { + return Response.status(Response.Status.FORBIDDEN).build(); + } + + @Path("/notacc") + @GET + public Response notacc() { + return Response.status(Response.Status.NOT_ACCEPTABLE).build(); + } + + @Path("/unsupported") + @GET + public Response unsupported() { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).build(); + } + + @Path("/error") + @GET + public Response error() { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + + @Path("/unavailable") + @GET + public Response unavailable() { + return Response.status(Response.Status.SERVICE_UNAVAILABLE).build(); + } + + @Path("/customClient") + @GET + public Response customClient() { + return Response.status(Response.Status.PRECONDITION_FAILED).build(); + } + + @Path("/customServer") + @GET + public Response customServer() { + return Response.status(Response.Status.BAD_GATEWAY).build(); + } + + @Path("/customRedirect") + @GET + public Response customRedirect() { + return Response.status(Response.Status.USE_PROXY).build(); + } + + + @Path("/informal") + @GET + public Response informal() { + return Response.status(700).build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FileResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FileResource.java new file mode 100644 index 000000000..7ef9c959e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FileResource.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.jetty.http.HttpHeader; + +/** + * @author Vyacheslav Rusakov + * @since 09.10.2025 + */ +@Path("/file") +@Produces(MediaType.APPLICATION_OCTET_STREAM) +public class FileResource { + + @GET + @Path("/download") + public Response download() { + return Response.ok(getClass().getResourceAsStream("/logback.xml")) + .header(HttpHeader.CONTENT_DISPOSITION.toString(), "attachment; filename=logback.xml") + .build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormBeanResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormBeanResource.java new file mode 100644 index 000000000..930d96d2c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormBeanResource.java @@ -0,0 +1,149 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.InputStream; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 10.10.2025 + */ +@Path("/formbean") +public class FormBeanResource { + + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotNull @BeanParam SimpleBean bean) { + return "name=" + bean.value + ", date=" + bean.date; + } + + public static class SimpleBean { + @FormParam("name") + public String value; + + @FormParam("date") + public String date; + + public SimpleBean() { + } + + public SimpleBean(String value, String date) { + this.value = value; + this.date = date; + } + } + + @Path("/postMulti") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String postMulti(@NotNull @BeanParam SimpleMultiBean bean) { + return "name=" + bean.value + ", date=" + bean.date; + } + + public static class SimpleMultiBean { + @FormParam("name") + public List value; + + @FormParam("date") + public String date; + + public SimpleMultiBean() { + } + + public SimpleMultiBean(List value, String date) { + this.value = value; + this.date = date; + } + } + + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart(@NotNull @BeanParam MultipartBean bean) { + return bean.detail.getFileName(true); + } + + public static class MultipartBean { + @FormDataParam("file") + public InputStream stream; + + @FormDataParam("file") + public FormDataContentDisposition detail; + + public MultipartBean() { + } + + public MultipartBean(InputStream stream, FormDataContentDisposition detail) { + this.stream = stream; + this.detail = detail; + } + } + + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2(@NotNull @BeanParam MultipartBean2 bean) { + return bean.file.getFileName().get(); + } + + public static class MultipartBean2 { + @FormDataParam("file") + public FormDataBodyPart file; + + public MultipartBean2() { + } + + public MultipartBean2(FormDataBodyPart file) { + this.file = file; + } + } + + @Path("/multipartMulti") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartMulti(@NotNull @BeanParam MultipartMultiBean bean) { + return bean.file.get(0).getFileName().get(); + } + + public static class MultipartMultiBean { + @FormDataParam("file") + public List file; + + public MultipartMultiBean() { + } + + public MultipartMultiBean(List file) { + this.file = file; + } + } + + @Path("/multipartMulti2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartMulti2(@NotNull @BeanParam MultipartMultiBean2 data) { + return data.file.get(0).getFileName(true); + } + + public static class MultipartMultiBean2 { + @FormDataParam("file") + public List file; + + public MultipartMultiBean2() { + } + + public MultipartMultiBean2(List file) { + this.file = file; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormResource.java new file mode 100644 index 000000000..ad0579abe --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/FormResource.java @@ -0,0 +1,114 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import com.google.common.base.Joiner; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * @author Vyacheslav Rusakov + * @since 10.10.2025 + */ +@Path("/form") +public class FormResource { + + @Path("/post") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post(@NotNull @FormParam("name") String value, + @NotNull @FormParam("date") String date) { + return "name=" + value + ", date=" + date; + } + + @Path("/post2") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post2(@NotEmpty MultivaluedMap params) { + return Joiner.on(", ").withKeyValueSeparator('=').join(params); + } + + @Path("/postMulti") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String postMulti(@NotEmpty @FormParam("name") List value, + @NotNull @FormParam("date") String date) { + return "name=" + value + ", date=" + date; + } + + @Path("/post2Multi") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String post2Multi(@NotEmpty MultivaluedMap params) { + return Joiner.on(", ").withKeyValueSeparator('=').join(params); + } + + @Path("/get") + @GET + public String get(@NotNull @QueryParam("name") String value, + @NotNull @QueryParam("date") String date) { + return "name=" + value + ", date=" + date; + } + + @Path("/getMulti") + @GET + public String getMulti(@NotEmpty @QueryParam("name") List value, + @NotNull @QueryParam("date") String date) { + return "name=" + value + ", date=" + date; + } + + @Path("/multipart") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart( + @NotNull @FormDataParam("file") InputStream uploadedInputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDetail) { + return fileDetail.getFileName(true); + } + + @Path("/multipart2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipart2( + @NotNull @FormDataParam("file") FormDataBodyPart file) { + return file.getFileName().get(); + } + + @Path("/multipartMulti") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartMulti( + @NotNull @FormDataParam("file") List file) { + return file.get(0).getFileName().get(); + } + + @Path("/multipartMulti2") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartMulti2( + @NotNull @FormDataParam("file") List file) { + return file.get(0).getFileName(true); + } + + @Path("/multipartGeneric") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String multipartGeneric(@NotNull FormDataMultiPart multiPart) { + Map> fieldsMap = multiPart.getFields(); + return fieldsMap.get("file").get(0).getFileName().get(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/MatrixResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/MatrixResource.java new file mode 100644 index 000000000..c838dde6c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/MatrixResource.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.PathSegment; +import ru.vyarus.dropwizard.guice.test.client.support.sub.SubMatrix; + +/** + * @author Vyacheslav Rusakov + * @since 13.10.2025 + */ +@Path("/matrix") +public class MatrixResource { + + @Path("/get") + @GET + public String get(@MatrixParam("p1") String p1, @MatrixParam("p2") String p2) { + return p1 + ";" + p2; + } + + // /get2;a=1/ + @Path("/{vars:get2}/op/") + @GET + public String get(@PathParam("vars") PathSegment vars, @MatrixParam("p1") String p1, @MatrixParam("p2") String p2) { + return p1 + ";" + p2; + } + + // /sub;a=1/ + @Path("/{vars:sub}") + public SubMatrix sub(@PathParam("vars") PathSegment vars) { + return new SubMatrix(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/PrimitivesResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/PrimitivesResource.java new file mode 100644 index 000000000..c42ad9ab0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/PrimitivesResource.java @@ -0,0 +1,67 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 13.10.2025 + */ +@Path("/primitive") +public class PrimitivesResource { + + @Path("/byte") + @GET + public byte getByte() { + return 1; + } + + @Path("/bytes") + @GET + public byte[] getBytes() { + return new byte[]{1}; + } + + @Path("/long") + @GET + public long getLong() { + return 1; + } + + @Path("/boolean") + @GET + public boolean getBoolean() { + return false; + } + + @Path("/short") + @GET + public short getShort() { + return 1; + } + + @Path("/float") + @GET + public float getFloat() { + return 1; + } + + @Path("/char") + @GET + public char getChar() { + return 1; + } + + @Path("/int") + @GET + public int getInt() { + return 1; + } + + @Path("/double") + @GET + public double getDouble() { + return 1; + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/Resource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/Resource.java new file mode 100644 index 000000000..8d9aa3238 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/Resource.java @@ -0,0 +1,121 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.RuntimeDelegate; +import org.eclipse.jetty.http.HttpHeader; +import ru.vyarus.dropwizard.guice.test.client.support.sub.SubResource; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * @author Vyacheslav Rusakov + * @since 07.10.2025 + */ +@Path("/root") +public class Resource { + + @GET + @Path("/get") + public List get() { + return Arrays.asList(1, 2, 3); + } + + @GET + @Path("/get/{name}") + public List get(@PathParam("name") String param) { + return Arrays.asList(4, 5, 6); + } + + @GET + @Path("/filled") + @Produces(MediaType.TEXT_PLAIN) + public Response filled() { + return Response.ok("OK") + .language(Locale.CANADA) + .header("HH", "3") + .header(HttpHeader.X_POWERED_BY.toString(), "4") + .cookie(new NewCookie.Builder("C").value("12").build()) + .cacheControl(RuntimeDelegate.getInstance().createHeaderDelegate(CacheControl.class) + .fromString("max-age=604800, must-revalidate")) + .build(); + } + + @DELETE + @Path("/del") + public void del() { + } + + @DELETE + @Path("/delete") + public int delete() { + return 1; + } + + @POST + @Path("/post") + public String post(String text) { + return text; + } + + @PUT + @Path("/put") + public String put(String text) { + return text; + } + + @PATCH + @Path("/patch") + public String patch(String text) { + return text; + } + + @Path("/sub") + public SubResource sub() { + return new SubResource(); + } + + @Path("/entity") + @POST + public String post2(ModelType model) { + return model.getName(); + } + + @Path("/entity2") + @POST + public String post3(@NotNull ModelType model) { + return model.getName(); + } + + public static class ModelType { + private String name; + + public ModelType() { + } + + public ModelType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/SuccFailRedirectResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/SuccFailRedirectResource.java new file mode 100644 index 000000000..db4887485 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/SuccFailRedirectResource.java @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.test.client.support; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import ru.vyarus.dropwizard.guice.url.AppUrlBuilder; + +/** + * @author Vyacheslav Rusakov + * @since 08.10.2025 + */ +@Path("/status") +public class SuccFailRedirectResource { + + @Inject + AppUrlBuilder urlBuilder; + + @Path("/get") + @GET + public String get() { + return "ok"; + } + + @Path("/post") + @POST + public String post(String entity) { + return entity; + } + + @Path("/error") + @GET + public String error() { + throw new IllegalStateException("err"); + } + + @Path("/redirect") + @GET + public Response redirect() { + return Response.seeOther( + urlBuilder.rest(SuccFailRedirectResource.class).method(SuccFailRedirectResource::get).buildUri() + ).build(); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubMatrix.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubMatrix.java new file mode 100644 index 000000000..5ffc0a6ce --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubMatrix.java @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.test.client.support.sub; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 13.10.2025 + */ +@Path("/msub") +public class SubMatrix { + + @Path("/get") + @GET + public String get(@MatrixParam("s1") String s1) { + return s1; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubResource.java new file mode 100644 index 000000000..a3b2a1409 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubResource.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.client.support.sub; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2025 + */ +@Path("/sub") +public class SubResource { + + @Path("/get") + @GET + public String get() { + return "ok"; + } + + @Path("/sub2") + public SubSubResource sub() { + return new SubSubResource(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubSubResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubSubResource.java new file mode 100644 index 000000000..c8235a523 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/client/support/sub/SubSubResource.java @@ -0,0 +1,18 @@ +package ru.vyarus.dropwizard.guice.test.client.support.sub; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2025 + */ +@Path("/sub2") +public class SubSubResource { + + @Path("/get") + @GET + public String get() { + return "ko"; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ApacheFactoryTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ApacheFactoryTest.java new file mode 100644 index 000000000..a7ff9d158 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ApacheFactoryTest.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +public class ApacheFactoryTest { + + @Test + void testApacheFactoryShortcut() throws Exception{ + Class cls = TestSupport.build(AutoScanApplication.class) + .apacheClient() + .runCore(injector -> TestSupport + .getContextClient().getClient().getConfiguration().getConnectorProvider().getClass()); + Assertions.assertEquals(Apache5ConnectorProvider.class, cls); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/AppStartupFailTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/AppStartupFailTest.java new file mode 100644 index 000000000..4773b9355 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/AppStartupFailTest.java @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Environment; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.cmd.CommandResult; + +/** + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +public class AppStartupFailTest { + + @Test + void testAppStartupFail() { + + final CommandResult res = TestSupport.buildCommandRunner(App.class).run("server"); + + Assertions.assertThat(res.getException().getMessage()).isEqualTo("Something went wrong"); + } + + @Test + void testStartupPrevention() { + final CommandResult res = TestSupport.buildCommandRunner(AutoScanApplication.class) + .runApp(); + + Assertions.assertThat(res.getException().getMessage()) + .isEqualTo("Application was expected to fail on startup, but successfully started instead"); + } + + @Test + void testAppCheck() { + final CommandResult res = TestSupport.buildCommandRunner(App.class) + .run("check"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput()).contains("Configuration is OK"); + } + + public static class App extends Application { + + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + throw new IllegalStateException("Something went wrong"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderConfigModifyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderConfigModifyTest.java new file mode 100644 index 000000000..c118ed03b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderConfigModifyTest.java @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.server.DefaultServerFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class BuilderConfigModifyTest { + + @Test + void testConfigModificationsApplied() throws Exception { + TestConfiguration conf = TestSupport.build(AutoScanApplication.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 2", "bar: 3", "baa: 4") + .configModifiers(config -> config.foo = 11) + .configModifiers(BarModifier.class, GenericModifier.class) + .runCore(injector -> injector.getInstance(TestConfiguration.class)); + + Assertions.assertEquals(11, conf.foo); + Assertions.assertEquals(12, conf.bar); + Assertions.assertEquals(4, conf.baa); + Assertions.assertEquals(22, ((DefaultServerFactory) conf.getServerFactory()).getAdminMaxThreads()); + } + + @Test + void testConfigModificationAppliedForInstance() throws Exception { + TestConfiguration conf = TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .configModifiers(config -> config.foo = 11) + .configModifiers(BarModifier.class, GenericModifier.class) + .runCore(injector -> injector.getInstance(TestConfiguration.class)); + + Assertions.assertEquals(11, conf.foo); + Assertions.assertEquals(12, conf.bar); + Assertions.assertEquals(0, conf.baa); + Assertions.assertEquals(22, ((DefaultServerFactory)conf.getServerFactory()).getAdminMaxThreads()); + } + + @Test + void testConfigModifyError() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> { + TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .configModifiers(config -> {throw new IllegalArgumentException("error");}) + .runCore(); + + }); + + Assertions.assertTrue(ex.getMessage().startsWith("Configuration modification failed for ru.vyarus.dropwizard.guice.test.general.BuilderConfigModifyTest")); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } + + public static class BarModifier implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + config.bar = 12; + } + } + + public static class GenericModifier implements ConfigModifier { + @Override + public void modify(Configuration config) throws Exception { + ((DefaultServerFactory) config.getServerFactory()).setAdminMaxThreads(22); + ; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderRunCoreWithoutManagedTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderRunCoreWithoutManagedTest.java new file mode 100644 index 000000000..eebbc7198 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderRunCoreWithoutManagedTest.java @@ -0,0 +1,107 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Singleton; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +public class BuilderRunCoreWithoutManagedTest { + + @Test + void testCoreRun() throws Exception { + + RunResult result = TestSupport.build(App.class).runCore(); + + UnusedManaged managed = result.getBean(UnusedManaged.class); + Assertions.assertTrue(managed.started); + Assertions.assertTrue(managed.stopped); + + Assertions.assertEquals(Arrays.asList("lifeCycleStarting", "lifeCycleStarted", "lifeCycleStopping", "lifeCycleStopped"), result.getApplication().events); + } + + @Test + void testCoreRunWithoutManagedLifecycle() throws Exception { + + RunResult result = TestSupport.build(App.class).runCoreWithoutManaged(); + + UnusedManaged managed = result.getBean(UnusedManaged.class); + Assertions.assertFalse(managed.started); + Assertions.assertFalse(managed.stopped); + + Assertions.assertEquals(Arrays.asList("lifeCycleStarting", "lifeCycleStarted", "lifeCycleStopping", "lifeCycleStopped"), result.getApplication().events); + } + + public static class App extends DefaultTestApp { + public final List events = new ArrayList<>(); + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(UnusedManaged.class) + .build(); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.lifecycle().addEventListener(new LifeCycle.Listener() { + @Override + public void lifeCycleStarting(LifeCycle event) { + events.add("lifeCycleStarting"); + } + + @Override + public void lifeCycleStarted(LifeCycle event) { + events.add("lifeCycleStarted"); + } + + @Override + public void lifeCycleFailure(LifeCycle event, Throwable cause) { + events.add("lifeCycleFailure"); + } + + @Override + public void lifeCycleStopping(LifeCycle event) { + events.add("lifeCycleStopping"); + } + + @Override + public void lifeCycleStopped(LifeCycle event) { + events.add("lifeCycleStopped"); + } + }); + // server listener is not called in any case (no reals server started) + environment.lifecycle().addServerLifecycleListener(server -> events.add("serverStarted")); + } + } + + @Singleton + public static class UnusedManaged implements Managed { + public boolean started = false; + public boolean stopped = false; + + @Override + public void start() throws Exception { + started = true; + } + + @Override + public void stop() throws Exception { + stopped = true; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderTest.java new file mode 100644 index 000000000..1712e1795 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/BuilderTest.java @@ -0,0 +1,215 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import com.google.common.base.Preconditions; +import com.google.inject.Injector; +import io.dropwizard.core.server.AbstractServerFactory; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.support.client.CustomTestClientFactory; +import ru.vyarus.dropwizard.guice.support.feature.DummyExceptionMapper; +import ru.vyarus.dropwizard.guice.support.feature.DummyManaged; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportBuilder; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +public class BuilderTest { + + @Test + void testCoreAppRun() throws Exception { + + final List tracker = new ArrayList<>(); + TestConfiguration config = build(tracker) + .runCore(injector -> { + Preconditions.checkNotNull(TestSupport.getContext()); + final ClientSupport client = TestSupport.getContextClient(); + Preconditions.checkNotNull(client); + + Preconditions.checkState(CustomTestClientFactory.getCalled() == 0); + client.getClient(); // trigger factory usage + + // hooks applied + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + Preconditions.checkState(info.getExtensionsDisabled().contains(DummyManaged.class)); + Preconditions.checkState(info.getExtensionsDisabled().contains(DummyExceptionMapper.class)); + + return injector.getInstance(TestConfiguration.class); + }); + + Assertions.assertIterableEquals(Arrays.asList("setup", "run", "stop", "cleanup"), tracker); + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(config.foo, 2); + Assertions.assertEquals(config.bar, 12); + Assertions.assertEquals(config.baa, 4); + } + + @Test + void testCoreAppCreation() throws Exception { + DropwizardTestSupport support = build(null).buildCore(); + + Assertions.assertNotNull(support); + Assertions.assertTrue(GuiceyTestSupport.class.isAssignableFrom(support.getClass())); + TestSupport.run(support); + + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(support.getConfiguration().foo, 2); + Assertions.assertEquals(support.getConfiguration().bar, 12); + Assertions.assertEquals(support.getConfiguration().baa, 4); + } + + @Test + void testWebAppRun() throws Exception { + + final List tracker = new ArrayList<>(); + TestConfiguration config = build(tracker) + .runWeb(injector -> { + Preconditions.checkNotNull(TestSupport.getContext()); + final ClientSupport client = TestSupport.getContextClient(); + Preconditions.checkNotNull(client); + + Preconditions.checkState(CustomTestClientFactory.getCalled() == 0); + client.getClient(); // trigger factory usage + + // hooks applied + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + Preconditions.checkState(info.getExtensionsDisabled().contains(DummyManaged.class)); + Preconditions.checkState(info.getExtensionsDisabled().contains(DummyExceptionMapper.class)); + + return injector.getInstance(TestConfiguration.class); + }); + + Assertions.assertIterableEquals(Arrays.asList("setup", "run", "stop", "cleanup"), tracker); + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(config.foo, 2); + Assertions.assertEquals(config.bar, 12); + Assertions.assertEquals(config.baa, 4); + } + + @Test + void testWebAppCreation() throws Exception { + DropwizardTestSupport support = build(null).buildWeb(); + + Assertions.assertNotNull(support); + Assertions.assertFalse(GuiceyTestSupport.class.isAssignableFrom(support.getClass())); + TestSupport.run(support); + + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(support.getConfiguration().foo, 2); + Assertions.assertEquals(support.getConfiguration().bar, 12); + Assertions.assertEquals(support.getConfiguration().baa, 4); + } + + @Test + void testRunWithoutCallback() throws Exception { + final List tracker = new ArrayList<>(); + RunResult res = build(tracker).runCore(); + + Assertions.assertTrue(GuiceyTestSupport.class.isAssignableFrom(res.getSupport().getClass())); + Assertions.assertIterableEquals(Arrays.asList("setup", "run", "stop", "cleanup"), tracker); + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(res.getConfiguration().foo, 2); + Assertions.assertEquals(res.getConfiguration().bar, 12); + Assertions.assertEquals(res.getConfiguration().baa, 4); + + + tracker.clear(); + res = build(tracker).runWeb(); + + Assertions.assertFalse(GuiceyTestSupport.class.isAssignableFrom(res.getSupport().getClass())); + Assertions.assertIterableEquals(Arrays.asList("setup", "run", "stop", "cleanup"), tracker); + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + Assertions.assertEquals(res.getConfiguration().foo, 2); + Assertions.assertEquals(res.getConfiguration().bar, 12); + Assertions.assertEquals(res.getConfiguration().baa, 4); + } + + @Test + void testSupportCreationWithListener() { + + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, + () -> build(new ArrayList<>()).buildCore()); + Assertions.assertEquals("Listeners could be used only with run* methods.", ex.getMessage()); + + + ex = Assertions.assertThrows(IllegalStateException.class, + () -> build(new ArrayList<>()).buildWeb()); + Assertions.assertEquals("Listeners could be used only with run* methods.", ex.getMessage()); + } + + @Test + void testRestMappingWithManualConfig() throws Exception { + final RunResult result = TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .restMapping("foo") + .runWeb(); + + Assertions.assertEquals("/foo/*", ((AbstractServerFactory) result.getConfiguration() + .getServerFactory()).getJerseyRootPath().get()); + } + + protected TestSupportBuilder build(List listenerTracker) { + TestSupportBuilder res = TestSupport.build(AutoScanApplication.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 2", "bar: 12") + .propertyPrefix("custom") + .randomPorts() + .restMapping("api") + .hooks(Hook.class) + .hooks(builder -> builder.disableExtensions(DummyManaged.class)) + .clientFactory(new CustomTestClientFactory()); + + if (listenerTracker != null) { + res.listen(new TestSupportBuilder.TestListener<>() { + @Override + public void setup(DropwizardTestSupport support) throws Exception { + Preconditions.checkNotNull(support); + listenerTracker.add("setup"); + } + + @Override + public void run(DropwizardTestSupport support, Injector injector) throws Exception { + Preconditions.checkNotNull(support); + Preconditions.checkNotNull(injector); + listenerTracker.add("run"); + } + + @Override + public void stop(DropwizardTestSupport support, Injector injector) throws Exception { + Preconditions.checkNotNull(support); + Preconditions.checkNotNull(injector); + listenerTracker.add("stop"); + } + + @Override + public void cleanup(DropwizardTestSupport support) throws Exception { + Preconditions.checkNotNull(support); + listenerTracker.add("cleanup"); + } + }); + } + return res; + } + + public static class Hook implements GuiceyConfigurationHook { + + @Override + public void configure(GuiceBundle.Builder builder) { + builder.disableExtensions(DummyExceptionMapper.class); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ClientSupportShortcutsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ClientSupportShortcutsTest.java new file mode 100644 index 000000000..d830b06fb --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ClientSupportShortcutsTest.java @@ -0,0 +1,297 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.client.DefaultTestClientFactory; + +/** + * @author Vyacheslav Rusakov + * @since 26.11.2023 + */ +public class ClientSupportShortcutsTest { + + @Test + void testShortcutMethods() throws Exception { + + TestSupport.runWebApp(App.class, injector -> { + ClientSupport client = TestSupport.getContextClient(); + + // GET + ResModel res = client.get("sample/get", ResModel.class); + Assertions.assertThat(res.getFoo()).isEqualTo("get"); + + client.get("sample/get2"); + + Assertions.assertThatThrownBy(() -> client.get("sample/getErr")) + .hasMessageContaining("HTTP 500 Server Error"); + Assertions.assertThatThrownBy(() -> client.get("sample/getErr2", ResModel.class)) + .hasMessageContaining("HTTP 500 Server Error"); + + + // POST + res = client.post("sample/post", null, ResModel.class); + Assertions.assertThat(res.getFoo()).isEqualTo("post"); + + res = client.post("sample/post2", new InModel("tes"), ResModel.class); + Assertions.assertThat(res.getFoo()).isEqualTo("tes"); + + client.post("sample/post3", null); + + Assertions.assertThatThrownBy(() -> client.post("sample/postErr", null)) + .hasMessageContaining("HTTP 500 Server Error"); + Assertions.assertThatThrownBy(() -> client.post("sample/postErr2", null, ResModel.class)) + .hasMessageContaining("HTTP 500 Server Error"); + + + // PUT + Assertions.assertThatThrownBy(() -> client.put("sample/put", null, ResModel.class)) + // does not allow null body + .hasMessageContaining("Entity must not be null for http method PUT"); + + res = client.put("sample/put2", new InModel("tes"), ResModel.class); + Assertions.assertThat(res.getFoo()).isEqualTo("tes"); + + client.put("sample/put3", new InModel("tt")); + + Assertions.assertThatThrownBy(() -> client.put("sample/putErr", new InModel("tt"))) + .hasMessageContaining("HTTP 500 Server Error"); + Assertions.assertThatThrownBy(() -> client.put("sample/putErr2", new InModel("tt"), ResModel.class)) + .hasMessageContaining("HTTP 500 Server Error"); + + + + // DELETE + res = client.delete("sample/del", ResModel.class); + Assertions.assertThat(res.getFoo()).isEqualTo("delete"); + + client.delete("sample/del2"); + + Assertions.assertThatThrownBy(() -> client.delete("sample/delErr")) + .hasMessageContaining("HTTP 500 Server Error"); + Assertions.assertThatThrownBy(() -> client.delete("sample/delErr2", ResModel.class)) + .hasMessageContaining("HTTP 500 Server Error"); + + return null; + }); + } + + @Test + void testClientOutputDisable() throws Exception { + + // run with client output + String out = TestSupport.captureOutput(() -> { + TestSupport.runWebApp(App.class, injector -> { + ClientSupport client = TestSupport.getContextClient(); + client.get("sample/get", Void.class); + + return null; + }); + }); + Assertions.assertThat(out) + .contains("[Client action]---------------------------------------------{"); + + // run with disabled output + DefaultTestClientFactory.disableConsoleLog(); + try { + out = TestSupport.captureOutput(() -> { + TestSupport.runWebApp(App.class, injector -> { + ClientSupport client = TestSupport.getContextClient(); + client.get("sample/get", Void.class); + + return null; + }); + }); + Assertions.assertThat(out) + .doesNotContain("[Client action]---------------------------------------------{") + // usual logger works instead + .contains("1 * Sending client request on thread"); + } finally { + DefaultTestClientFactory.enableConsoleLog(); + } + + // make sure output enabled again + out = TestSupport.captureOutput(() -> { + TestSupport.runWebApp(App.class, injector -> { + ClientSupport client = TestSupport.getContextClient(); + client.get("sample/get", Void.class); + + return null; + }); + }); + Assertions.assertThat(out) + .contains("[Client action]---------------------------------------------{"); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(SampleRest.class); + } + } + + @Produces("application/json") + @Path("sample/") + public static class SampleRest { + + @GET + @Path("/get") + public ResModel get() { + return new ResModel("get"); + } + + @GET + @Path("/get2") + public void get2() { + } + + @GET + @Path("/getErr") + public void getErrorVoid() { + throw new IllegalStateException("err"); + } + + @GET + @Path("/getErr2") + public ResModel getError() { + throw new IllegalStateException("err"); + } + + @POST + @Path("/post") + public ResModel post() { + return new ResModel("post"); + } + + @POST + @Path("/post2") + public ResModel post2(InModel in) { + return new ResModel(in.bar); + } + + @POST + @Path("/post3") + public void post3() { + } + + @POST + @Path("/postErr") + public void postErrorVoid() { + throw new IllegalStateException("err"); + } + + @POST + @Path("/postErr2") + public ResModel postError() { + throw new IllegalStateException("err"); + } + + @PUT + @Path("/put") + public ResModel put() { + return new ResModel("put"); + } + + @PUT + @Path("/put2") + public ResModel put2(InModel in) { + return new ResModel(in.bar); + } + + @PUT + @Path("/put3") + public void put3(InModel in) { + } + + @PUT + @Path("/putErr") + public void putErrorVoid() { + throw new IllegalStateException("err"); + } + + @PUT + @Path("/putErr2") + public ResModel putError() { + throw new IllegalStateException("err"); + } + + @DELETE + @Path("/del") + public ResModel delete() { + return new ResModel("delete"); + } + + @DELETE + @Path("/del2") + public void delete2() { + } + + @DELETE + @Path("/delErr") + public void deleteErrorVoid() { + throw new IllegalStateException("err"); + } + + @DELETE + @Path("/delErr2") + public ResModel deleteError() { + throw new IllegalStateException("err"); + } + } + + public static class ResModel { + private String foo; + + public ResModel() { + } + + public ResModel(String foo) { + this.foo = foo; + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + } + + public static class InModel { + private String bar; + + public InModel() { + } + + public InModel(String bar) { + this.bar = bar; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CommandRunTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CommandRunTest.java new file mode 100644 index 000000000..ccfa97831 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CommandRunTest.java @@ -0,0 +1,404 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.dropwizard.configuration.ResourceConfigurationSourceProvider; +import io.dropwizard.core.Application; +import io.dropwizard.core.cli.Command; +import io.dropwizard.core.cli.ConfiguredCommand; +import io.dropwizard.core.cli.EnvironmentCommand; +import io.dropwizard.core.server.DefaultServerFactory; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.cmd.CommandResult; +import ru.vyarus.dropwizard.guice.test.cmd.CommandRunBuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +/** + * @author Vyacheslav Rusakov + * @since 20.11.2023 + */ +public class CommandRunTest { + + @Test + void testSimpleCommandRun() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .run("simple", "-u", "user"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).contains("Hello user"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNull(); + Assertions.assertThat(res.getEnvironment()).isNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNull(); + + + res = TestSupport.buildCommandRunner(App.class) + .run("simple"); + + Assertions.assertThat(res.isSuccess()).isFalse(); + Assertions.assertThat(res.getOutput()).contains("usage: java -jar project.jar simple -u USER [-h]"); + Assertions.assertThat(res.getErrorOutput()).contains("usage: java -jar project.jar simple -u USER [-h]"); + Assertions.assertThat(res.getException()).isExactlyInstanceOf(ArgumentParserException.class); + Assertions.assertThat(res.getConfiguration()).isNull(); + } + + @Test + void testConfiguredCommandRun() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .configOverride("foo: 12") + .run("cfg"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).endsWith("foo value: 12"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNotNull(); + Assertions.assertThat(res.getConfiguration().foo).isEqualTo(12); + Assertions.assertThat(res.getEnvironment()).isNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNull(); + + + // configuration from file + res = TestSupport.buildCommandRunner(App.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverride("foo: 12") + .run("cfg"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).endsWith("foo value: 12"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNotNull(); + Assertions.assertThat(res.getConfiguration().foo).isEqualTo(12); + Assertions.assertThat(res.getConfiguration().bar).isEqualTo(3); + Assertions.assertThat(res.getEnvironment()).isNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNull(); + + + // configuration from object + TestConfiguration config = new TestConfiguration(); + config.foo = 12; + config.bar = 3; + res = TestSupport.buildCommandRunner(App.class) + .config(config) + .run("cfg"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).endsWith("foo value: 12"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNotNull(); + Assertions.assertThat(res.getConfiguration().foo).isEqualTo(12); + Assertions.assertThat(res.getConfiguration().bar).isEqualTo(3); + Assertions.assertThat(res.getEnvironment()).isNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNull(); + } + + @Test + void testEnvironmentCommandRun() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .configOverride("foo: 11") + .run("env"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).contains("foo value: 11"); + Assertions.assertThat(res.getOutput().trim()).contains("service 11"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNotNull(); + Assertions.assertThat(res.getConfiguration().foo).isEqualTo(11); + Assertions.assertThat(res.getEnvironment()).isNotNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNotNull(); + Assertions.assertThat(res.getInjector().getInstance(FooService.class).isCalled()).isTrue(); + } + + @Test + void testRunWithInput() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .consoleInputs("one", "two") + .run("input"); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).isEqualTo("user input: one two"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNull(); + Assertions.assertThat(res.getEnvironment()).isNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNull(); + + + res = TestSupport.buildCommandRunner(App.class) + // incomplete input + .consoleInputs("one") + .run("input"); + + Assertions.assertThat(res.isSuccess()).isFalse(); + Assertions.assertThat(res.getOutput()).contains("Console input (2) not provided"); + Assertions.assertThat(res.getErrorOutput()).contains("Console input (2) not provided"); + Assertions.assertThat(res.getException()).isExactlyInstanceOf(IllegalStateException.class); + Assertions.assertThat(res.getConfiguration()).isNull(); + } + + @Test + void testObjectAndConfigUsedTogether() { + Assertions.assertThatThrownBy(() -> TestSupport.buildCommandRunner(App.class) + .config(new TestConfiguration()) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .run("env")).hasMessage("Configuration object can't be used together with yaml configuration"); + } + + @Test + void testConfigObjectWithConfigProvider() { + Assertions.assertThatThrownBy(() -> TestSupport.buildCommandRunner(App.class) + .config(new TestConfiguration()) + .configSourceProvider(new ResourceConfigurationSourceProvider()) + .run("env")).hasMessage("Configuration object can't be used together with yaml configuration"); + } + + @Test + void testCommandRunFail() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .run("err"); + + Assertions.assertThat(res.isSuccess()).isFalse(); + Assertions.assertThat(res.getOutput()).contains("Error in command"); + Assertions.assertThat(res.getErrorOutput()).contains("Error in command"); + Assertions.assertThat(res.getException()).isExactlyInstanceOf(IllegalStateException.class); + Assertions.assertThat(res.getException().getMessage()).isEqualTo("Error in command"); + Assertions.assertThat(res.getConfiguration()).isNull(); + } + + @Test + void testNoArgsRun() { + CommandResult res = TestSupport.buildCommandRunner(App.class) + .run(); + + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput()).contains("usage: java -jar project.jar"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNull(); + } + + + @Test + void testCommandRunOptions() { + System.setProperty("custom.foo", "11"); + final List track = new ArrayList<>(); + CommandResult res = TestSupport.buildCommandRunner(App.class) + .propertyPrefix("custom") + .listen(new CommandRunBuilder.CommandListener<>() { + @Override + public void setup(String[] args) { + Preconditions.checkState(args.length == 1 && "env".equals(args[0])); + track.add("setup"); + } + + @Override + public void cleanup(CommandResult result) { + Preconditions.checkNotNull(result); + Preconditions.checkState(result.getInjector().getInstance(FooService.class).isCalled()); + track.add("cleanup"); + } + }) + .hooks(builder -> track.add("hook")) + .run("env"); + + Assertions.assertThatList(track).isEqualTo(Arrays.asList("setup", "hook", "cleanup")); + Assertions.assertThat(res.isSuccess()).isTrue(); + Assertions.assertThat(res.getOutput().trim()).contains("foo value: 11"); + Assertions.assertThat(res.getOutput().trim()).contains("service 11"); + Assertions.assertThat(res.getErrorOutput()).isEmpty(); + Assertions.assertThat(res.getException()).isNull(); + Assertions.assertThat(res.getConfiguration()).isNotNull(); + Assertions.assertThat(res.getConfiguration().foo).isEqualTo(11); + Assertions.assertThat(res.getEnvironment()).isNotNull(); + Assertions.assertThat(res.getApplication()).isNotNull(); + Assertions.assertThat(res.getCommand()).isNotNull(); + Assertions.assertThat(res.getBootstrap()).isNotNull(); + Assertions.assertThat(res.getInjector()).isNotNull(); + Assertions.assertThat(res.getInjector().getInstance(FooService.class).isCalled()).isTrue(); + } + + @Test + void testConfigModificationsApplied() { + final CommandResult res = TestSupport.buildCommandRunner(App.class) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 2", "bar: 3", "baa: 4") + .configModifiers(config -> config.foo = 11) + .configModifiers(BuilderConfigModifyTest.BarModifier.class, BuilderConfigModifyTest.GenericModifier.class) + .run("env"); + + final TestConfiguration config = res.getConfiguration(); + Assertions.assertThat(config.foo).isEqualTo(11); + Assertions.assertThat(config.bar).isEqualTo(12); + Assertions.assertThat(config.baa).isEqualTo(4); + Assertions.assertThat(((DefaultServerFactory) config.getServerFactory()).getAdminMaxThreads()).isEqualTo(22); + } + + @Test + void testConfigModificationAppliedForInstance() { + final CommandResult res = TestSupport.buildCommandRunner(App.class) + .config(new TestConfiguration()) + .configModifiers(config -> config.foo = 11) + .configModifiers(BuilderConfigModifyTest.BarModifier.class, BuilderConfigModifyTest.GenericModifier.class) + .run("env"); + + final TestConfiguration config = res.getConfiguration(); + Assertions.assertThat(config.foo).isEqualTo(11); + Assertions.assertThat(config.bar).isEqualTo(12); + Assertions.assertThat(config.baa).isEqualTo(0); + Assertions.assertThat(((DefaultServerFactory) config.getServerFactory()).getAdminMaxThreads()).isEqualTo(22); + } + + public static class App extends Application { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addCommand(new SimpleCommand()); + bootstrap.addCommand(new ConfCommand()); + bootstrap.addCommand(new EnvCommand(this)); + bootstrap.addCommand(new InputCommand()); + bootstrap.addCommand(new ThrowingCommand()); + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + + } + } + + public static class SimpleCommand extends Command { + + public SimpleCommand() { + super("simple", "Simple command"); + } + + @Override + public void configure(Subparser subparser) { + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("The user of the program"); + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + System.out.println("Hello " + namespace.getString("user")); + } + } + + public static class ConfCommand extends ConfiguredCommand { + public ConfCommand() { + super("cfg", "Configured command"); + } + + @Override + protected void run(Bootstrap bootstrap, Namespace namespace, TestConfiguration configuration) throws Exception { + System.out.println("foo value: " + configuration.foo); + } + } + + public static class EnvCommand extends EnvironmentCommand { + + @Inject + FooService service; + + public EnvCommand(Application application) { + super(application, "env", "Environment command"); + } + + @Override + protected void run(Environment environment, Namespace namespace, TestConfiguration configuration) throws Exception { + System.out.println("foo value: " + configuration.foo + "; " + service.something()); + } + } + + @Singleton + public static class FooService { + + @Inject + private TestConfiguration conf; + private boolean called; + + + public String something() { + this.called = true; + return "service " + conf.foo; + } + + public boolean isCalled() { + return called; + } + } + + public static class InputCommand extends Command { + + public InputCommand() { + super("input", "command with user input"); + } + + @Override + public void configure(Subparser subparser) { + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + Scanner in = new Scanner(System.in); + + String line = in.nextLine(); + String line2 = in.nextLine(); + System.out.println("user input: " + line + " " + line2); + } + } + + public static class ThrowingCommand extends Command { + + public ThrowingCommand() { + super("err", "Command with error"); + } + + @Override + public void configure(Subparser subparser) { + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + throw new IllegalStateException("Error in command"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ConfigObjectBuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ConfigObjectBuilderTest.java new file mode 100644 index 000000000..58f2fb783 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/ConfigObjectBuilderTest.java @@ -0,0 +1,88 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.configuration.ResourceConfigurationSourceProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +/** + * @author Vyacheslav Rusakov + * @since 17.11.2023 + */ +public class ConfigObjectBuilderTest { + + @Test + void testCoreRunWithConfigObject() throws Exception { + TestConfiguration config = new TestConfiguration(); + config.foo = 2; + config.bar = 12; + + RunResult res = TestSupport.build(AutoScanApplication.class) + .config(config).runCore(); + Assertions.assertEquals(res.getConfiguration(), config); + Assertions.assertEquals(res.getConfiguration().foo, 2); + Assertions.assertEquals(res.getConfiguration().bar, 12); + } + + @Test + void testWebRunWithConfigObject() throws Exception { + TestConfiguration config = new TestConfiguration(); + config.foo = 2; + config.bar = 12; + + RunResult support = TestSupport.build(AutoScanApplication.class) + .config(config).runWeb(); + Assertions.assertEquals(support.getConfiguration(), config); + Assertions.assertEquals(support.getConfiguration().foo, 2); + Assertions.assertEquals(support.getConfiguration().bar, 12); + } + + @Test + void testConfigObjectWithConfigPath() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> + TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .config("/path/to/config") + .runCore()); + + ex.printStackTrace(); + Assertions.assertEquals("Configuration object can't be used together with yaml configuration", ex.getMessage()); + + + ex = Assertions.assertThrows(IllegalStateException.class, () -> + TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .config("/path/to/config") + .runWeb()); + + ex.printStackTrace(); + Assertions.assertEquals("Configuration object can't be used together with yaml configuration", ex.getMessage()); + } + + @Test + void testConfigObjectWithOverride() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> + TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .configOverride("foo", "12") + .runCore()); + + ex.printStackTrace(); + Assertions.assertEquals("Configuration object can't be used together with yaml configuration", ex.getMessage()); + } + + @Test + void testConfigObjectWithConfigProvider() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> + TestSupport.build(AutoScanApplication.class) + .config(new TestConfiguration()) + .configSourceProvider(new ResourceConfigurationSourceProvider()) + .runCore()); + + ex.printStackTrace(); + Assertions.assertEquals("Configuration object can't be used together with yaml configuration", ex.getMessage()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CustomPrefixTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CustomPrefixTest.java new file mode 100644 index 000000000..ad65500a9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/CustomPrefixTest.java @@ -0,0 +1,54 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 18.11.2023 + */ +public class CustomPrefixTest { + @Test + void testDefaultPrefix() throws Exception { + + DropwizardTestSupport support = TestSupport.build(AutoScanApplication.class) + .configOverride("foo: 11") + .restMapping("rest") + .buildCore(); + + try { + // apply properties + support.before(); + + Assertions.assertEquals("11", System.getProperty("dw.foo")); + Assertions.assertEquals("/rest/*", System.getProperty("dw.server.rootPath")); + } finally { + support.after(); + } + } + + + @Test + void testPrefixApplied() throws Exception { + + DropwizardTestSupport support = TestSupport.build(AutoScanApplication.class) + .propertyPrefix("ttt") + .configOverride("foo: 11") + .restMapping("rest") + .buildCore(); + + try { + // apply properties + support.before(); + + Assertions.assertEquals("11", System.getProperty("ttt.foo")); + Assertions.assertEquals("/rest/*", System.getProperty("ttt.server.rootPath")); + } finally { + support.after(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/GuiceyTestSupportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/GuiceyTestSupportTest.java new file mode 100644 index 000000000..5a39f5758 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/GuiceyTestSupportTest.java @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import com.google.common.base.Preconditions; +import com.google.inject.Key; +import io.dropwizard.configuration.FileConfigurationSourceProvider; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.core.Configuration; +import org.apache.commons.text.StringSubstitutor; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.support.feature.DummyService; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 22.11.2023 + */ +public class GuiceyTestSupportTest { + + @Test + void testGuiceySupportManualUsage() throws Exception { + GuiceyTestSupport support = new GuiceyTestSupport<>(AutoScanApplication.class, (String) null); + support.before(); + Assertions.assertThat(support.getBean(DummyService.class)).isNotNull(); + Assertions.assertThat(support.getBean(Key.get(DummyService.class))).isNotNull(); + support.after(); + + Assertions.assertThatThrownBy(() -> support.getBean(DummyService.class)) + .hasMessage("Guice injector not available"); + } + + @Test + void testRunWithinStartedSupport() throws Exception { + GuiceyTestSupport support = new GuiceyTestSupport<>(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", + new SubstitutingSourceProvider(new FileConfigurationSourceProvider(), new StringSubstitutor())); + + support.run(injector -> { + Preconditions.checkNotNull(injector.getInstance(DummyService.class)); + return null; + }); + } + + @Test + void testRunWithoutManagedLifecycle() throws Exception { + GuiceyTestSupport support = new GuiceyTestSupport<>(BuilderRunCoreWithoutManagedTest.App.class, (String) null) + .disableManagedLifecycle(); + + RunResult result = support.run(); + BuilderRunCoreWithoutManagedTest.UnusedManaged managed = result.getBean(BuilderRunCoreWithoutManagedTest.UnusedManaged.class); + Assertions.assertThat(managed.started).isFalse(); + Assertions.assertThat(managed.stopped).isFalse(); + + org.junit.jupiter.api.Assertions.assertEquals(Arrays.asList("lifeCycleStarting", "lifeCycleStarted", "lifeCycleStopping", "lifeCycleStopped"), + result.getApplication().events); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/TestSupportShortcutsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/TestSupportShortcutsTest.java new file mode 100644 index 000000000..0fcc309e2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/general/TestSupportShortcutsTest.java @@ -0,0 +1,115 @@ +package ru.vyarus.dropwizard.guice.test.general; + +import com.google.common.base.Preconditions; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.support.feature.DummyService; +import ru.vyarus.dropwizard.guice.test.GuiceyTestSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.util.RunResult; + +/** + * @author Vyacheslav Rusakov + * @since 17.11.2023 + */ +public class TestSupportShortcutsTest { + + @Test + void testSupportConstruction() throws Exception { + final GuiceyTestSupport support = TestSupport + .coreApp(AutoScanApplication.class, null, "foo: 11"); + TestSupport.run(support); + Assertions.assertEquals(11, support.getConfiguration().foo); + + DropwizardTestSupport support2 = TestSupport + .webApp(AutoScanApplication.class, null, "foo: 11"); + TestSupport.run(support2); + Assertions.assertEquals(11, support2.getConfiguration().foo); + } + + @Test + void testCoreRun() throws Exception { + RunResult res = TestSupport.runCoreApp(AutoScanApplication.class); + Assertions.assertNotNull(res.getSupport()); + Assertions.assertNotNull(res.getConfiguration()); + Assertions.assertNotNull(res.getApplication()); + Assertions.assertNotNull(res.getEnvironment()); + Assertions.assertNotNull(res.getInjector()); + Assertions.assertNotNull(res.getBean(DummyService.class)); + Assertions.assertFalse(res.isWebRun()); + + + DropwizardTestSupport support = TestSupport.runCoreApp(AutoScanApplication.class, + injector -> { + Preconditions.checkNotNull(injector); + Preconditions.checkNotNull(TestSupport.getContextClient()); + return Preconditions.checkNotNull(TestSupport.getContext()); + }); + Assertions.assertNotNull(support.getConfiguration()); + + + res = TestSupport.runCoreApp(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", "foo: 2", "bar: 12"); + Assertions.assertEquals(2, res.getConfiguration().foo); + + + support = TestSupport.runCoreApp(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", + injector -> { + Preconditions.checkNotNull(injector); + Preconditions.checkNotNull(TestSupport.getContextClient()); + return Preconditions.checkNotNull(TestSupport.getContext()); + }, "foo: 2", "bar: 12"); + Assertions.assertEquals(2, support.getConfiguration().foo); + } + + + @Test + void testWebRun() throws Exception { + RunResult res = TestSupport.runWebApp(AutoScanApplication.class); + Assertions.assertNotNull(res.getSupport()); + Assertions.assertNotNull(res.getConfiguration()); + Assertions.assertNotNull(res.getApplication()); + Assertions.assertNotNull(res.getEnvironment()); + Assertions.assertNotNull(res.getInjector()); + Assertions.assertNotNull(res.getBean(DummyService.class)); + Assertions.assertTrue(res.isWebRun()); + + + DropwizardTestSupport support = TestSupport.runWebApp(AutoScanApplication.class, + injector -> { + Preconditions.checkNotNull(injector); + Preconditions.checkNotNull(TestSupport.getContextClient()); + return Preconditions.checkNotNull(TestSupport.getContext()); + }); + Assertions.assertNotNull(support.getConfiguration()); + + + res = TestSupport.runWebApp(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", "foo: 2", "bar: 12"); + Assertions.assertEquals(2, res.getConfiguration().foo); + + + support = TestSupport.runWebApp(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", + injector -> { + Preconditions.checkNotNull(injector); + Preconditions.checkNotNull(TestSupport.getContextClient()); + return Preconditions.checkNotNull(TestSupport.getContext()); + }, "foo: 2", "bar: 12"); + Assertions.assertEquals(2, support.getConfiguration().foo); + + + support = TestSupport.runWebApp(AutoScanApplication.class, + "src/test/resources/ru/vyarus/dropwizard/guice/config.yml", + injector -> { + Preconditions.checkNotNull(injector); + Preconditions.checkNotNull(TestSupport.getContextClient()); + return Preconditions.checkNotNull(TestSupport.getContext()); + }, "foo: 2", "bar: 12"); + Assertions.assertEquals(2, support.getConfiguration().foo); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy index 92b8f6ad3..5b344faa9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy @@ -3,6 +3,7 @@ package ru.vyarus.dropwizard.guice.test.hook import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker import spock.lang.Specification /** @@ -24,7 +25,7 @@ class ConfigurationHooksSupportTest extends Specification { ConfigurationHooksSupport.count() == 1 when: "check processing" - ConfigurationHooksSupport.run(GuiceBundle.builder()) + ConfigurationHooksSupport.run(GuiceBundle.builder(), new StatsTracker()) then: "hooks flushed" ConfigurationHooksSupport.count() == 0 @@ -41,7 +42,7 @@ class ConfigurationHooksSupportTest extends Specification { when: ({ init = true } as GuiceyConfigurationHook).register() - ConfigurationHooksSupport.run(GuiceBundle.builder()) + ConfigurationHooksSupport.run(GuiceBundle.builder(), new StatsTracker()) then: "called" init diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DelayedConfigInHookTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DelayedConfigInHookTest.java new file mode 100644 index 000000000..08e754c88 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DelayedConfigInHookTest.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.hook; + +import com.google.inject.Inject; +import io.dropwizard.lifecycle.Managed; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ExtensionItemInfo; +import ru.vyarus.dropwizard.guice.module.context.info.ItemId; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 16.03.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class DelayedConfigInHookTest { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder + .extensions(Mng1.class) + .whenConfigurationReady(environment -> environment.extensions(Mng2.class)) + .printDiagnosticInfo(); + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testCorrectRegistrationContext() { + Assertions.assertTrue(info.getExtensions().contains(Mng1.class)); + Assertions.assertTrue(info.getExtensions().contains(Mng2.class)); + + ExtensionItemInfo ext = info.getInfo(Mng1.class); + Assertions.assertEquals(1, ext.getRegisteredBy().size()); + Assertions.assertEquals(ItemId.from(GuiceyConfigurationHook.class), ext.getRegisteredBy().iterator().next()); + + ext = info.getInfo(Mng2.class); + Assertions.assertEquals(1, ext.getRegisteredBy().size()); + Assertions.assertEquals(ItemId.from(GuiceyConfigurationHook.class), ext.getRegisteredBy().iterator().next()); + } + + + public static class Mng1 implements Managed {} + public static class Mng2 implements Managed {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy new file mode 100644 index 000000000..e9a1cfd3d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Inject +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.hook.DiagnosticHook +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 18.08.2019 + */ +@TestGuiceyApp(App) +class DiagnosticHookEnableTest extends Specification { + + void cleanup() { + ConfigurationHooksSupport.reset() + } + + @Inject + GuiceyConfigurationInfo info + + def "Diagnostic hook enable"() { + + expect: + info.getData().getHooks().contains(DiagnosticHook) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + System.setProperty(ConfigurationHooksSupport.HOOKS_PROPERTY, DiagnosticHook.ALIAS) + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DuplicateBuildCallTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DuplicateBuildCallTest.java new file mode 100644 index 000000000..3546e4e15 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DuplicateBuildCallTest.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.test.hook; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; + +/** + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ +public class DuplicateBuildCallTest { + + @AfterEach + void tearDown() { + ConfigurationHooksSupport.reset(); + } + + @Test + void testDuplicateBuildCall() { + ((GuiceyConfigurationHook) GuiceBundle.Builder::build).register(); + final IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> new DefaultTestApp().run("server")); + org.assertj.core.api.Assertions.assertThat(ex.getMessage()) + .isEqualTo(".build() was already called for guice bundle. Most likely, it was called second time in GuiceyConfigurationHook"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceProvisionEnableHookTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceProvisionEnableHookTest.groovy new file mode 100644 index 000000000..e313bb21a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceProvisionEnableHookTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Inject +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.hook.GuiceProvisionTimeHook +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2025 + */ +@TestGuiceyApp(App) +class GuiceProvisionEnableHookTest extends Specification { + + void cleanup() { + ConfigurationHooksSupport.reset() + } + + @Inject + GuiceyConfigurationInfo info + + def "Guice provision hook enable"() { + + expect: + info.getData().getHooks().contains(GuiceProvisionTimeHook) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + System.setProperty(ConfigurationHooksSupport.HOOKS_PROPERTY, GuiceProvisionTimeHook.ALIAS) + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/StartupDiagnosticEnableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/StartupDiagnosticEnableTest.groovy new file mode 100644 index 000000000..a8ea00177 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/StartupDiagnosticEnableTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Inject +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.hook.StartupTimeHook +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 14.03.2025 + */ +@TestGuiceyApp(App) +class StartupDiagnosticEnableTest extends Specification { + + void cleanup() { + ConfigurationHooksSupport.reset() + } + + @Inject + GuiceyConfigurationInfo info + + def "Startup hook enable"() { + + expect: + info.getData().getHooks().contains(StartupTimeHook) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + System.setProperty(ConfigurationHooksSupport.HOOKS_PROPERTY, StartupTimeHook.ALIAS) + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java index 3e21fe9af..c8893936f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/GenericSupportTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -14,10 +14,10 @@ import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.TestSupport; -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.ws.rs.GET; -import javax.ws.rs.Path; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java index e14995041..066259deb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/HooksIndependentDeclarationTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java index ce59101c2..64b0fd34a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/InjectionScopeTest.java @@ -7,8 +7,8 @@ import org.junit.jupiter.api.TestMethodOrder; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java new file mode 100644 index 000000000..b42d37dea --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.jupiter; + +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; + +import jakarta.inject.Inject; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 01.05.2020 + */ +@TestGuiceyApp(AutoScanApplication.class) +public class NestedPropagationTest { + + @Inject + Environment environment; + + @Test + void checkInjection(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertNotNull(environment); + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + + @Nested + class Inner { + + @Inject + Environment env; // intentionally different name + + @Test + void checkInjection(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertNotNull(env); + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java similarity index 65% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java index a40484c70..57607fcc4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedTreeTest.java @@ -1,12 +1,14 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; +import jakarta.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; - -import javax.inject.Inject; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; /** * @author Vyacheslav Rusakov @@ -32,8 +34,9 @@ class Level2 { Environment env; @Test - void checkExtensionApplied() { + void checkExtensionApplied(DropwizardTestSupport support) { Assertions.assertNotNull(env); + Assertions.assertEquals(support, TestSupport.getContext()); } @Nested @@ -43,8 +46,9 @@ class Level3 { Environment envr; @Test - void checkExtensionApplied() { + void checkExtensionApplied(DropwizardTestSupport support) { Assertions.assertNotNull(envr); + Assertions.assertEquals(support, TestSupport.getContext()); } } } @@ -58,6 +62,7 @@ class NotAffected { @Test void extensionNotApplied() { Assertions.assertNull(environment); + Assertions.assertFalse(TestSupportHolder.isContextSet()); } } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java similarity index 79% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java index 8ac7c5639..8c4c57577 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionPerMethodTest.java @@ -1,5 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter; +import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -7,9 +8,10 @@ import org.junit.platform.testkit.engine.EngineTestKit; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.TestSupport; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; -import javax.inject.Inject; +import jakarta.inject.Inject; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; @@ -50,13 +52,15 @@ public static class Test1 { TestConfiguration config; @Test - void check() { + void check(DropwizardTestSupport support) { Assertions.assertEquals(1, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test - void check2() { + void check2(DropwizardTestSupport support) { Assertions.assertEquals(1, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test @@ -83,13 +87,15 @@ public static class Test2 { TestConfiguration config; @Test - void check() { + void check(DropwizardTestSupport support) { Assertions.assertEquals(2, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test - void check2() { + void check2(DropwizardTestSupport support) { Assertions.assertEquals(2, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test @@ -116,13 +122,15 @@ public static class Test3 { TestConfiguration config; @Test - void check() { + void check(DropwizardTestSupport support) { Assertions.assertEquals(3, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test - void check2() { + void check2(DropwizardTestSupport support) { Assertions.assertEquals(3, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test @@ -149,13 +157,15 @@ public static class Test4 { TestConfiguration config; @Test - void check() { + void check(DropwizardTestSupport support) { Assertions.assertEquals(4, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test - void check2() { + void check2(DropwizardTestSupport support) { Assertions.assertEquals(4, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } @Test diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java similarity index 78% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java index 4953ca65f..b33271294 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/ParallelExecutionTest.java @@ -1,5 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter; +import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -7,6 +8,7 @@ import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; @@ -41,8 +43,9 @@ void checkParallelExecution() { public static class Test1 { @Test - void check(ClientSupport client) { + void check(ClientSupport client, DropwizardTestSupport support) { Assertions.assertEquals(20000, client.getPort()); + Assertions.assertEquals(support, TestSupport.getContext()); } } @@ -54,8 +57,9 @@ void check(ClientSupport client) { public static class Test2 { @Test - void check(ClientSupport client) { + void check(ClientSupport client, DropwizardTestSupport support) { Assertions.assertEquals(10000, client.getPort()); + Assertions.assertEquals(support, TestSupport.getContext()); } } @@ -66,8 +70,9 @@ void check(ClientSupport client) { public static class Test3 { @Test - void check(TestConfiguration config) { + void check(TestConfiguration config, DropwizardTestSupport support) { Assertions.assertEquals(1, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } } @@ -78,8 +83,9 @@ void check(TestConfiguration config) { public static class Test4 { @Test - void check(TestConfiguration config) { + void check(TestConfiguration config, DropwizardTestSupport support) { Assertions.assertEquals(2, config.foo); + Assertions.assertEquals(support, TestSupport.getContext()); } } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java index a5fb12dd0..0d2a6e989 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/QualifiedAndParameterizedParamInjectionTest.java @@ -4,16 +4,16 @@ import com.google.inject.Provides; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.GuiceBundle; -import javax.inject.Named; -import javax.inject.Provider; +import jakarta.inject.Named; +import jakarta.inject.Provider; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java index 862a89679..b262d6c96 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StartErrorJupiterTest.java @@ -2,22 +2,20 @@ import com.google.inject.AbstractModule; import com.google.inject.name.Named; -import io.dropwizard.Application; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.support.TestConfiguration; import ru.vyarus.dropwizard.guice.support.feature.DummyCommand; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; import uk.org.webcompere.systemstubs.jupiter.SystemStub; import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.security.SystemExit; import uk.org.webcompere.systemstubs.stream.SystemErr; -import uk.org.webcompere.systemstubs.stream.SystemOut; - -import javax.inject.Inject; /** * @author Vyacheslav Rusakov @@ -26,29 +24,25 @@ @ExtendWith(SystemStubsExtension.class) public class StartErrorJupiterTest { - @SystemStub - SystemExit exit; @SystemStub SystemErr err; @Test // NOTE could be parallelized with some tests, but not with other error tests void checkStartupFail() throws Exception { - exit.execute(() -> { - ErrorApplication.main("server"); - }); - Assertions.assertEquals(1, exit.getExitCode()); + Assertions.assertThrows(RuntimeException.class, () -> ErrorApplication.main("server")); + + Assertions.assertFalse(TestSupportHolder.isContextSet()); } @Test // NOTE not parallelizable test! void checkStartupFailWithOutput() throws Exception { - exit.execute(() -> { - ErrorApplication.main("server"); - }); + Assertions.assertThrows(RuntimeException.class, () -> ErrorApplication.main("server")); // strange matching because in java9 @Named value will be quoted and in 8 will not Assertions.assertTrue(err.getText() .contains("[Guice/MissingImplementation]: No implementation for String annotated with @Named")); + Assertions.assertFalse(TestSupportHolder.isContextSet()); } @@ -69,6 +63,11 @@ public void initialize(Bootstrap bootstrap) { @Override public void run(TestConfiguration configuration, Environment environment) throws Exception { } + + @Override + protected void onFatalError(Throwable t) { + throw new RuntimeException(t); + } } public static class ErrorModule extends AbstractModule { diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextAccessTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextAccessTest.java new file mode 100644 index 000000000..a98bf5eb7 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextAccessTest.java @@ -0,0 +1,97 @@ +package ru.vyarus.dropwizard.guice.test.jupiter; + +import com.google.common.base.Preconditions; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +@TestGuiceyApp(AutoScanApplication.class) +public class StaticContextAccessTest { + + // generified support injection also works + public StaticContextAccessTest(DropwizardTestSupport support, ClientSupport client) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupport.getContext()); + Preconditions.checkState(client == TestSupport.getContextClient()); + } + + @BeforeAll + static void before(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @BeforeEach + void setUp(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @AfterEach + void tearDown(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @AfterAll + static void after(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @Test + void test1(DropwizardTestSupport support, + ClientSupport client) { + assertNotNull(client); + assertNotNull(support); + assertEquals(support, TestSupport.getContext()); + assertEquals(client, TestSupport.getContextClient()); + } + + @Test + void test2(DropwizardTestSupport support, + ClientSupport client) { + assertNotNull(client); + assertNotNull(support); + assertEquals(support, TestSupport.getContext()); + assertEquals(client, TestSupport.getContextClient()); + } + + @Nested + class Inner { + + @Test + void test1(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + + @Test + void test2(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextPerMethodAccessTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextPerMethodAccessTest.java new file mode 100644 index 000000000..72773e8a9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/StaticContextPerMethodAccessTest.java @@ -0,0 +1,92 @@ +package ru.vyarus.dropwizard.guice.test.jupiter; + +import com.google.common.base.Preconditions; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +public class StaticContextPerMethodAccessTest { + @RegisterExtension + TestGuiceyAppExtension extension = TestGuiceyAppExtension.forApp(AutoScanApplication.class).create(); + + // parameters can't be used in constructor and beforeAll/afterAll due to per-method execution + public StaticContextPerMethodAccessTest() { + Preconditions.checkState(!TestSupportHolder.isContextSet()); + } + + @BeforeAll + static void before() { + Preconditions.checkState(!TestSupportHolder.isContextSet()); + } + + @BeforeEach + void setUp(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @AfterEach + void tearDown(DropwizardTestSupport support) { + Preconditions.checkNotNull(support); + Preconditions.checkState(TestSupportHolder.isContextSet()); + Preconditions.checkState(support == TestSupportHolder.getContext()); + } + + @AfterAll + static void after() { + Preconditions.checkState(!TestSupportHolder.isContextSet()); + } + + @Test + void test1(DropwizardTestSupport support, + ClientSupport client) { + assertNotNull(client); + assertNotNull(support); + assertEquals(support, TestSupport.getContext()); + assertEquals(client, TestSupport.getContextClient()); + } + + @Test + void test2(DropwizardTestSupport support, + ClientSupport client) { + assertNotNull(client); + assertNotNull(support); + assertEquals(support, TestSupport.getContext()); + assertEquals(client, TestSupport.getContextClient()); + } + + @Nested + class Inner { + + @Test + void test1(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + + @Test + void test2(DropwizardTestSupport support, ClientSupport client) { + Assertions.assertEquals(support, TestSupport.getContext()); + Assertions.assertEquals(client, TestSupport.getContextClient()); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java index e1e4e1b9f..ee41645cb 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/VoidExtensionsTest.java @@ -1,15 +1,15 @@ package ru.vyarus.dropwizard.guice.test.jupiter; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.GuiceBundle; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java new file mode 100644 index 000000000..a9fc189be --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java @@ -0,0 +1,129 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.debug; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; + +import static com.google.common.truth.Truth.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 25.06.2022 + */ +public class ConfigOverrideLogTest extends AbstractPlatformTest { + + @Test + void checkSetupOutputForAnnotation() { + String output = run(Test1.class); + + assertThat(output).contains("Configuration overrides (Test1.):\n" + + "\t bar = 11\n" + + "\t foo = 1\n" + + "\n" + + "Configuration modifiers:\n" + + "\t\tCfgModify1 \t@TestGuiceyApp(configModifiers)\n" + + "\t\t \t@EnableSetup Test1#setup.configModifiers(obj) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:85)\n" + + "\t\tCfgModify2 \t@EnableSetup Test1#setup.configModifiers(class) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:86)"); + + assertThat(output).contains( + "Guicey time after [Before each] of ConfigOverrideLogTest$Test1#test(): 111 ms \n" + + "\n" + + "\t[Before all] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuice fields injection : 111 ms"); + } + + @Test + void checkSetupOutputForManualRegistration() { + String output = run(Test2.class); + + assertThat(output).contains("Configuration overrides (Test2.):\n" + + "\t foo = 2\n" + + "\t bar = 11\n" + + "\n" + + "Configuration modifiers:\n" + + "\t\t \t@RegisterExtension.configModifiers(obj) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:100)\n" + + "\t\tCfgModify1 \t@RegisterExtension.configModifiers(class) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:101)\n" + + "\t\t \t@EnableSetup Test2#setup.configModifiers(obj) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:107)\n" + + "\t\tCfgModify2 \t@EnableSetup Test2#setup.configModifiers(class) at r.v.d.g.t.j.d.ConfigOverrideLogTest.(ConfigOverrideLogTest.java:108)"); + + assertThat(output).contains( + "Guicey time after [Before each] of ConfigOverrideLogTest$Test2#test(): 111 ms \n" + + "\n" + + "\t[Before all] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuice fields injection : 111 ms "); + } + + @Disabled // prevent direct execution + @TestGuiceyApp(value = AutoScanApplication.class, configOverride = "foo: 1", + configModifiers = CfgModify1.class, debug = true) + public static class Test1 { + + @EnableSetup + static TestEnvironmentSetup setup = ext -> + ext.configModifiers(config -> {}) + .configModifiers(CfgModify2.class) + .configOverride("bar", "11"); + + @Test + void test() { + } + } + + @Disabled // prevent direct execution + public static class Test2 { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .configOverrides("foo: 2") + .configModifiers(config -> {}) + .configModifiers(CfgModify1.class) + .debug() + .create(); + + @EnableSetup + static TestEnvironmentSetup setup = ext -> ext + .configModifiers(config -> {}) + .configModifiers(CfgModify2.class) + .configOverride("bar", "11"); + + @Test + void test() { + } + + } + + public static class CfgModify1 implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + } + } + + public static class CfgModify2 extends CfgModify1 {} + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java new file mode 100644 index 000000000..406cca351 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java @@ -0,0 +1,182 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.debug; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import static com.google.common.truth.Truth.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 29.05.2022 + */ +public class HookObjectsLogTest extends AbstractPlatformTest { + + @Test + void checkSetupOutputForAnnotation() { + String output = run(Test1.class); + assertThat(output).contains("Guicey test extensions (Test1.):\n" + + "\n" + + "\tSetup objects = \n" + + "\t\t \t@EnableSetup Test1#setup at r.v.d.g.t.j.d.HookObjectsLogTest$Test1#setup\n" + + "\t\tWebClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tWebResourceClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tLogFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tRestStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tMockFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tSpyFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\t\tTrackerFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\n" + + "\tTest hooks = \n" + + "\t\t \t@EnableHook Base#base1 at r.v.d.g.t.j.d.HookObjectsLogTest$Base#base1\n" + + "\t\t \t@EnableHook Base#base2 at r.v.d.g.t.j.d.HookObjectsLogTest$Base#base2\n" + + "\t\tExt1 \t@TestGuiceyApp(hooks)\n" + + "\t\tExt2 \t@TestGuiceyApp(hooks)\n" + + "\t\tExt3 \t@EnableSetup Test1#setup.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:140)\n" + + "\t\tExt4 \t@EnableSetup Test1#setup.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:140)\n" + + "\t\t \t@EnableSetup Test1#setup.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:141)\n" + + "\t\tExt5 \t@EnableSetup Test1#setup.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:141)\n" + + "\t\t \t@EnableHook Test1#ext1 at r.v.d.g.t.j.d.HookObjectsLogTest$Test1#ext1\n" + + "\t\t \t@EnableHook Test1#ext2 at r.v.d.g.t.j.d.HookObjectsLogTest$Test1#ext2"); + + assertThat(output).contains( + "Guicey time after [Before each] of HookObjectsLogTest$Test1#test(): 111 ms \n" + + "\n" + + "\t[Before all] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuice fields injection : 111 ms"); + } + + @Test + void checkSetupOutputForManualRegistration() { + String output = run(Test2.class); + assertThat(output).contains("Guicey test extensions (Test2.):\n" + + "\n" + + "\tSetup objects = \n" + + "\t\t \t@EnableSetup Test2#setup at r.v.d.g.t.j.d.HookObjectsLogTest$Test2#setup\n" + + "\t\tWebClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tWebResourceClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tLogFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tRestStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tMockFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tSpyFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\t\tTrackerFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\n" + + "\tTest hooks = \n" + + "\t\t \t@EnableHook Base#base1 at r.v.d.g.t.j.d.HookObjectsLogTest$Base#base1\n" + + "\t\t \t@EnableHook Base#base2 at r.v.d.g.t.j.d.HookObjectsLogTest$Base#base2\n" + + "\t\tExt1 \t@RegisterExtension.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:158)\n" + + "\t\tExt2 \t@RegisterExtension.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:158)\n" + + "\t\t \t@RegisterExtension.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:159)\n" + + "\t\t \t@RegisterExtension.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:159)\n" + + "\t\tExt3 \t@EnableSetup Test2#setup.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:165)\n" + + "\t\tExt4 \t@EnableSetup Test2#setup.hooks(class) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:165)\n" + + "\t\t \t@EnableSetup Test2#setup.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:166)\n" + + "\t\t \t@EnableSetup Test2#setup.hooks(obj) at r.v.d.g.t.j.d.HookObjectsLogTest.(HookObjectsLogTest.java:166)\n" + + "\t\t \t@EnableHook Test2#ext1 at r.v.d.g.t.j.d.HookObjectsLogTest$Test2#ext1\n" + + "\t\tExt5 \t@EnableHook Test2#ext2 at r.v.d.g.t.j.d.HookObjectsLogTest$Test2#ext2\n"); + + assertThat(output).contains( + "Guicey time after [Before each] of HookObjectsLogTest$Test2#test(): 111 ms \n" + + "\n" + + "\t[Before all] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuice fields injection : 111 ms"); + } + + public static class Base { + + @EnableHook + static GuiceyConfigurationHook base1 = it -> {}; + @EnableHook + static GuiceyConfigurationHook base2 = it -> {}; + } + + public static class Ext1 implements GuiceyConfigurationHook { + + @Override + public void configure(GuiceBundle.Builder builder) { + } + } + + public static class Ext2 extends Ext1 {} + + public static class Ext3 extends Ext1 {} + + public static class Ext4 extends Ext1 {} + + public static class Ext5 extends Ext1 {} + + + @Disabled // prevent direct execution + @TestGuiceyApp(value = AutoScanApplication.class, hooks = {Ext1.class, Ext2.class}, debug = true) + public static class Test1 extends Base { + + @EnableSetup + static TestEnvironmentSetup setup = it -> it + .hooks(Ext3.class, Ext4.class) + .hooks(t -> {}, new Ext5()); + + @EnableHook + static GuiceyConfigurationHook ext1 = it -> {}; + @EnableHook + static GuiceyConfigurationHook ext2 = it -> {}; + + @Test + void test() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .hooks(Ext1.class, Ext2.class) + .hooks(it -> {}, it -> {}) + .debug() + .create(); + + @EnableSetup + static TestEnvironmentSetup setup = it -> it + .hooks(Ext3.class, Ext4.class) + .hooks(t -> {}, t -> {}); + + @EnableHook + static GuiceyConfigurationHook ext1 = it -> {}; + @EnableHook + static GuiceyConfigurationHook ext2 = new Ext5(); + + @Test + void test() { + } + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java similarity index 50% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java index 8ff806de5..240958244 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/NestedConfigOverrideLogTest.java @@ -2,55 +2,42 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; -import ru.vyarus.dropwizard.guice.test.TestSupport; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.stream.SystemOut; import static com.google.common.truth.Truth.assertThat; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; /** * @author Vyacheslav Rusakov * @since 25.06.2022 */ -@ExtendWith(SystemStubsExtension.class) -public class NestedConfigOverrideLogTest { - - - @SystemStub - SystemOut out; +public class NestedConfigOverrideLogTest extends AbstractPlatformTest { @Test void checkSetupOutputForAnnotation() { Test1.i = 1; - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test1.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(2)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); + String output = run(Test1.class); assertThat(output).contains("Guicey test extensions (Test1.Inner.test1.):"); - assertThat(output).contains("Applied configuration overrides (Test1.Inner.test1.): \n" + - "\n" + + assertThat(output).contains("Configuration overrides (Test1.Inner.test1.):\n" + "\t foo = 1"); assertThat(output).contains("Guicey test extensions (Test1.Inner.test2.):"); - assertThat(output).contains("Applied configuration overrides (Test1.Inner.test2.): \n" + - "\n" + + assertThat(output).contains("Configuration overrides (Test1.Inner.test2.):\n" + "\t foo = 2"); + + assertThat(output).contains( + "Guicey time after [Before each] of Inner#test1(): 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\t\tGuice fields injection : 111 ms"); } public static class Test1 { @@ -73,4 +60,9 @@ void test1() {} void test2() {} } } + + @Override + protected String clean(String out) { + return unifyMs(out); + } } diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/PerformanceLogTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/PerformanceLogTest.java new file mode 100644 index 000000000..c991997e6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/PerformanceLogTest.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.debug; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2025 + */ +public class PerformanceLogTest extends AbstractPlatformTest { + + @Test + void testIterativePerformanceLog() { + + String output = run(Test1.class); + + Assertions.assertThat(output).contains("Guicey time after [Before each] of PerformanceLogTest$Test1#test1(): 111 ms \n" + + "\n" + + "\t[Before all] : 111 ms \n" + + "\t\tGuicey fields search : 111 ms \n" + + "\t\tGuicey hooks registration : 111 ms \n" + + "\t\tGuicey setup objects execution : 111 ms \n" + + "\t\tDropwizardTestSupport creation : 111 ms \n" + + "\t\tApplication start : 111 ms \n" + + "\n" + + "\t[Before each] : 111 ms \n" + + "\t\tGuice fields injection : 111 ms"); + + Assertions.assertThat(output).contains("Guicey time after [Before each] of PerformanceLogTest$Test1#test2(): 111 ms ( + 111 ms )\n" + + "\n" + + "\t[Before each] : 111 ms ( + 111 ms )\n" + + "\t\tGuice fields injection : 111 ms ( + 111 ms )\n" + + "\n" + + "\t[After each] : 111 ms"); + + Assertions.assertThat(output).contains("Guicey time after [After all] of PerformanceLogTest$Test1: 111 ms ( + 111 ms )\n" + + "\n" + + "\t[After each] : 111 ms ( + 111 ms )\n" + + "\n" + + "\t[After all] : 111 ms \n" + + "\t\tApplication stop : 111 ms"); + } + + @Disabled + @TestGuiceyApp(value = AutoScanApplication.class, debug = true) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + public static class Test1 { + + @Test + @Order(1) + void test1() { + } + + @Test + @Order(1) + void test2() { + } + } + + @Override + protected String clean(String out) { + return unifyMs(out); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java new file mode 100644 index 000000000..51c9cbe9d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java @@ -0,0 +1,125 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.debug; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import static com.google.common.truth.Truth.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 29.05.2022 + */ +public class SetupObjectsLogTest extends AbstractPlatformTest { + + @Test + void checkSetupOutputForAnnotation() { + String output = run(Test1.class); + assertThat(output).contains("Guicey test extensions (Test1.):\n" + + "\n" + + "\tSetup objects = \n" + + "\t\tExt1 \t@TestGuiceyApp(setup)\n" + + "\t\tExt2 \t@TestGuiceyApp(setup)\n" + + "\t\t \t@EnableSetup Base#base1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base1\n" + + "\t\t \t@EnableSetup Base#base2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base2\n" + + "\t\t \t@EnableSetup Test1#ext1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test1#ext1\n" + + "\t\tExt3 \t@EnableSetup Test1#ext2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test1#ext2\n" + + "\t\tWebClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tWebResourceClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tLogFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tRestStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tMockFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tSpyFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\t\tTrackerFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n"); + } + + @Test + void checkSetupOutputForManualRegistration() { + String output = run(Test2.class); + assertThat(output).contains("Guicey test extensions (Test2.):\n" + + "\n" + + "\tSetup objects = \n" + + "\t\tExt1 \t@RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:106)\n" + + "\t\tExt2 \t@RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:106)\n" + + "\t\t \t@RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:107)\n" + + "\t\tExt3 \t@RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:107)\n" + + "\t\t \t@EnableSetup Base#base1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base1\n" + + "\t\t \t@EnableSetup Base#base2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base2\n" + + "\t\t \t@EnableSetup Test2#ext1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext1\n" + + "\t\t \t@EnableSetup Test2#ext2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext2\n" + + "\t\tWebClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tWebResourceClientFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tLogFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tRestStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tStubFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tMockFieldsSupport \tlookup (service loader) at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:449)\n" + + "\t\tSpyFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n" + + "\t\tTrackerFieldsSupport \tdefault extension at r.v.d.g.t.j.ext.(GuiceyExtensionsSupport.java:453)\n"); + } + + public static class Base { + + @EnableSetup + static TestEnvironmentSetup base1 = it -> null; + @EnableSetup + static TestEnvironmentSetup base2 = it -> null; + } + + public static class Ext1 implements TestEnvironmentSetup { + @Override + public Object setup(TestExtension extension) { + return null; + } + } + + public static class Ext2 extends Ext1 {} + + public static class Ext3 extends Ext1 {} + + @Disabled // prevent direct execution + @TestGuiceyApp(value = AutoScanApplication.class, setup = {Ext1.class, Ext2.class}, debug = true) + public static class Test1 extends Base { + + @EnableSetup + static TestEnvironmentSetup ext1 = it -> null; + @EnableSetup + static TestEnvironmentSetup ext2 = new Ext3(); + + @Test + void test() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .setup(Ext1.class, Ext2.class) + .setup(it -> null, new Ext3()) + .debug() + .create(); + + @EnableSetup + static TestEnvironmentSetup ext1 = it -> null; + @EnableSetup + static TestEnvironmentSetup ext2 = it -> null; + + @Test + void test() { + } + } + + @Override + protected String clean(String out) { + return out.replaceAll("\\d+\\.\\d+ ms", "111 ms"); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDw.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDw.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDw.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDw.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java index 4066b058d..a70d03e61 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/AnnotatedBaseDwTest.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ApacheClientFactoryDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ApacheClientFactoryDwTest.java new file mode 100644 index 000000000..6dd2b8176 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ApacheClientFactoryDwTest.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +/** + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +@TestDropwizardApp(value = AutoScanApplication.class, apacheClient = true) +public class ApacheClientFactoryDwTest { + + @Test + void testApacheClientConfiguration(ClientSupport client) { + Assertions.assertEquals(Apache5ConnectorProvider.class, + client.getClient().getConfiguration().getConnectorProvider().getClass()); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java index 2a92aad36..1244f5fb2 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ClientSupportDwTest.java @@ -1,22 +1,22 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.GuiceBundle; -import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.GET; -import javax.ws.rs.Path; import java.io.IOException; import java.io.PrintWriter; @@ -29,14 +29,17 @@ public class ClientSupportDwTest { interface ClientCallTest { @Test default void callClient(ClientSupport client) { + Assertions.assertEquals("main", client.targetApp("servlet") + .request().buildGet().invoke().readEntity(String.class)); Assertions.assertEquals("main", client.targetMain("servlet") .request().buildGet().invoke().readEntity(String.class)); + Assertions.assertEquals("main", client.appClient().get("servlet", String.class)); Assertions.assertEquals("admin", client.targetAdmin("servlet") .request().buildGet().invoke().readEntity(String.class)); + Assertions.assertEquals("admin", client.adminClient().get("servlet", String.class)); - Assertions.assertEquals("ok", client.targetRest("sample") - .request().buildGet().invoke().readEntity(String.class)); + Assertions.assertEquals("ok", client.restClient().get("sample", String.class)); } } @@ -46,12 +49,15 @@ class DefaultConfig implements ClientCallTest { @Test void testClient(ClientSupport client) { - Assertions.assertEquals("http://localhost:8080/", client.basePathMain()); + Assertions.assertEquals("http://localhost:8080/", client.basePathRoot()); + Assertions.assertEquals("http://localhost:8080/", client.basePathApp()); Assertions.assertEquals("http://localhost:8081/", client.basePathAdmin()); Assertions.assertEquals("http://localhost:8080/", client.basePathRest()); - Assertions.assertEquals("main", client.target("http://localhost:8080", "servlet") + Assertions.assertEquals("main", client.target("http://localhost:8080/servlet") .request().buildGet().invoke().readEntity(String.class)); + Assertions.assertEquals("main", client.externalClient("http://localhost:8080/") + .get("servlet", String.class)); Assertions.assertEquals("main", client.getClient().target("http://localhost:8080/servlet") .request().buildGet().invoke().readEntity(String.class)); @@ -67,7 +73,8 @@ void testClient(ClientSupport client) { Assertions.assertNotEquals(8080, client.getPort()); Assertions.assertNotEquals(8081, client.getAdminPort()); - Assertions.assertEquals("http://localhost:" + client.getPort() + "/", client.basePathMain()); + Assertions.assertEquals("http://localhost:" + client.getPort() + "/", client.basePathRoot()); + Assertions.assertEquals("http://localhost:" + client.getPort() + "/", client.basePathApp()); Assertions.assertEquals("http://localhost:" + client.getAdminPort() + "/", client.basePathAdmin()); Assertions.assertEquals("http://localhost:" + client.getPort() + "/", client.basePathRest()); } @@ -83,6 +90,8 @@ class ChangedDefaultConfig implements ClientCallTest { @Test void testClient(ClientSupport client) { + Assertions.assertEquals("http://localhost:8080/", client.basePathRoot()); + Assertions.assertEquals("http://localhost:8080/app/", client.basePathApp()); Assertions.assertEquals("http://localhost:8080/app/", client.basePathMain()); Assertions.assertEquals("http://localhost:8081/admin/", client.basePathAdmin()); Assertions.assertEquals("http://localhost:8080/app/api/", client.basePathRest()); @@ -95,7 +104,8 @@ class SimpleConfig implements ClientCallTest { @Test void testClient(ClientSupport client) { - Assertions.assertEquals("http://localhost:8080/", client.basePathMain()); + Assertions.assertEquals("http://localhost:8080/", client.basePathRoot()); + Assertions.assertEquals("http://localhost:8080/", client.basePathApp()); Assertions.assertEquals("http://localhost:8080/admin/", client.basePathAdmin()); Assertions.assertEquals("http://localhost:8080/rest/", client.basePathRest()); } diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyDwTest.java new file mode 100644 index 000000000..4bae85444 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyDwTest.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class ConfigModifyDwTest { + + @RegisterExtension + static TestDropwizardAppExtension ext = TestDropwizardAppExtension.forApp(AutoScanApplication.class) + .configOverrides("foo: 2", "bar: 3", "baa: 4") + .configModifiers(config -> { + config.foo = 12; + }) + .debug() + .create(); + + + @EnableSetup + static TestEnvironmentSetup setup = ext -> + ext.configModifiers(config -> config.bar = 11); + + @Inject + TestConfiguration configuration; + + @Test + void testConfigModification() { + Assertions.assertEquals(12, configuration.foo); + Assertions.assertEquals(11, configuration.bar); + Assertions.assertEquals(4, configuration.baa); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyWithClassDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyWithClassDwTest.java new file mode 100644 index 000000000..0c31ce9a3 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigModifyWithClassDwTest.java @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.server.DefaultServerFactory; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +@TestDropwizardApp(value = AutoScanApplication.class, configModifiers = ConfigModifyWithClassDwTest.FooModifier.class, + configOverride = {"foo: 2", "bar: 3", "baa: 4"}) +public class ConfigModifyWithClassDwTest { + + @EnableSetup + static TestEnvironmentSetup setup = ext -> ext.configModifiers(BarModifier.class, GenericModifier.class); + + @Inject + TestConfiguration configuration; + + @Test + void testConfigModification() { + Assertions.assertEquals(11, configuration.foo); + Assertions.assertEquals(12, configuration.bar); + Assertions.assertEquals(4, configuration.baa); + Assertions.assertEquals(22, ((DefaultServerFactory) configuration.getServerFactory()).getAdminMaxThreads()); + } + + public static class FooModifier implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + config.foo = 11; + } + } + + public static class BarModifier implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + config.bar = 12; + } + } + + public static class GenericModifier implements ConfigModifier { + @Override + public void modify(Configuration config) throws Exception { + ((DefaultServerFactory)config.getServerFactory()).setAdminMaxThreads(22);; + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideDwTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideDwTest.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java index 642fadf79..1d54e87c5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ConfigOverrideFromExtensionDwTest.java @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; import com.fasterxml.jackson.annotation.JsonProperty; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomClientFactoryDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomClientFactoryDwTest.java new file mode 100644 index 000000000..bc2112d06 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomClientFactoryDwTest.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.client.CustomTestClientFactory; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +@TestDropwizardApp(value = AutoScanApplication.class, clientFactory = CustomTestClientFactory.class) +public class CustomClientFactoryDwTest { + + @Test + void testCustomClientFactory(ClientSupport client) { + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + client.getClient(); // force client creation + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java index 4b5da9c5b..78e2ec491 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/CustomRestMappingTest.java @@ -5,8 +5,8 @@ import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java index 10ee5bccb..2b3059cf4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/HooksDwTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.GuiceBundle; @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; import ru.vyarus.dropwizard.guice.test.jupiter.guicey.HooksGuiceyTest; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigDwTest.java new file mode 100644 index 000000000..27c0299cf --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigDwTest.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class ManualConfigDwTest { + + @RegisterExtension + static TestDropwizardAppExtension ext = TestDropwizardAppExtension.forApp(AutoScanApplication.class) + .config(() -> { + TestConfiguration res = new TestConfiguration(); + res.baa = 33; + return res; + }) + .configModifiers(config -> config.foo = 11) + .create(); + + @Inject + TestConfiguration config; + + @Test + void testManualConfiguration() { + Assertions.assertEquals(11, config.foo); + Assertions.assertEquals(33, config.baa); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigRestPathTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigRestPathTest.java new file mode 100644 index 000000000..735464403 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualConfigRestPathTest.java @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import io.dropwizard.core.server.DefaultServerFactory; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 25.04.2025 + */ +public class ManualConfigRestPathTest { + + @RegisterExtension + static TestDropwizardAppExtension ext = TestDropwizardAppExtension.forApp(AutoScanApplication.class) + .config(() -> { + TestConfiguration res = new TestConfiguration(); + res.baa = 33; + return res; + }) + .restMapping("/foo") + .create(); + + @Inject + TestConfiguration config; + + @Test + void testRestMappingShortcutApplied() { + + Assertions.assertEquals("/foo/*", + ((DefaultServerFactory)config.getServerFactory()).getJerseyRootPath().orElse(null)); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegApacheClientDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegApacheClientDwTest.java new file mode 100644 index 000000000..da0c8e3b0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegApacheClientDwTest.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +public class ManualRegApacheClientDwTest { + + @RegisterExtension + static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(AutoScanApplication.class) + .randomPorts() + .apacheClient() + .create(); + + @Test + void testApacheClientConfiguration(ClientSupport client) { + Assertions.assertEquals(Apache5ConnectorProvider.class, + client.getClient().getConfiguration().getConnectorProvider().getClass()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegInjectOnceDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegInjectOnceDwTest.java new file mode 100644 index 000000000..f77d3e5c2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegInjectOnceDwTest.java @@ -0,0 +1,72 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import com.google.inject.Inject; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.guicey.ManualRegInjectOnceGuiceyTest; + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManualRegInjectOnceDwTest { + + + @RegisterExtension + TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(ManualRegInjectOnceGuiceyTest.App.class) + .injectOnce() + .debug() + .create(); + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // same test instance + Assertions.assertEquals(testId, System.identityHashCode(this)); + // same bean (no second injection - prototype not replaced) + Assertions.assertEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java index 67d0f786a..306d80b21 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ManualRegistrationDwTest.java @@ -9,13 +9,14 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.support.client.CustomTestClientFactory; import ru.vyarus.dropwizard.guice.support.feature.DummyExceptionMapper; import ru.vyarus.dropwizard.guice.support.feature.DummyManaged; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; import ru.vyarus.dropwizard.guice.test.ClientSupport; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Response; /** * @author Vyacheslav Rusakov @@ -31,6 +32,7 @@ public class ManualRegistrationDwTest { .restMapping("api") .hooks(Hook.class) .hooks(builder -> builder.disableExtensions(DummyManaged.class)) + .clientFactory(new CustomTestClientFactory()) .create(); @Inject @@ -56,6 +58,10 @@ void checkCorrectWiring(GuiceyConfigurationInfo info, ClientSupport client) { .invoke(); Assertions.assertEquals(200, response.getStatus()); + + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + client.getClient(); // force factory call + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); } public static class Hook implements GuiceyConfigurationHook { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java index e1ba54645..d1098c961 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MetaAnnotationDwTest.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MultipartSupportInClientTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MultipartSupportInClientTest.java new file mode 100644 index 000000000..3fdddffbe --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/MultipartSupportInClientTest.java @@ -0,0 +1,96 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import com.google.common.base.Preconditions; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.forms.MultiPartBundle; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.spockframework.util.IoUtil; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.io.InputStream; +import java.nio.file.Files; + +/** + * @author Vyacheslav Rusakov + * @since 18.11.2023 + */ +@TestDropwizardApp(MultipartSupportInClientTest.App.class) +public class MultipartSupportInClientTest { + + @Test + void testMultipartClientSupport(@TempDir java.nio.file.Path temp, ClientSupport client) throws Exception { + java.nio.file.Path file = temp.resolve("sample.txt"); + Files.createFile(file); + Files.writeString(file, "sample content"); + + FormDataMultiPart multiPart = new FormDataMultiPart(); + multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + + FileDataBodyPart fileDataBodyPart = new FileDataBodyPart("file", + file.toFile(), + MediaType.APPLICATION_OCTET_STREAM_TYPE); + multiPart.bodyPart(fileDataBodyPart); + + + Response response = client.targetRest("form/handle").request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(multiPart, multiPart.getMediaType())); + + Assertions.assertEquals(200, response.getStatus()); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new MultiPartBundle()); + bootstrap.addBundle(GuiceBundle.builder() + .extensions(MultipartRest.class) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } + + @Path("form/") + @Produces(MediaType.APPLICATION_JSON) + public static class MultipartRest { + + + @POST + @Path("/handle") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response convert(@NotNull @FormDataParam("file") final InputStream uploadedInputStream, + @NotNull @FormDataParam("file") final FormDataContentDisposition fileDetail) + throws Exception { + final String text = IoUtil.getText(uploadedInputStream); + System.out.println("TEXT: " + text); + Preconditions.checkState(!text.isEmpty()); + + System.out.println("NAME: " + fileDetail.getFileName()); + Preconditions.checkNotNull(fileDetail.getFileName()); + return Response.status(200).build(); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java index 51e49b9b7..6bdb8963c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ObjectConfigOverrideDwTest.java @@ -30,6 +30,7 @@ public class ObjectConfigOverrideDwTest { static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(AutoScanApplication.class) .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") .configOverrides("foo: 1") + .configOverride("boo", "2") .configOverride("bar", () -> ext.getValue()) .configOverrides(new ConfigOverrideValue("baa", () -> "44")) .create(); @@ -48,6 +49,7 @@ void checkCorrectWiring() { Assertions.assertEquals(config.foo, 1); Assertions.assertEquals(config.bar, 22); Assertions.assertEquals(config.baa, 44); + Assertions.assertEquals(config.boo, 2); } public static class FooExtension implements BeforeAllCallback { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java index 229fd270e..fc470213d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ParametersInjectionDwTest.java @@ -3,14 +3,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; import com.google.inject.Injector; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; import ru.vyarus.dropwizard.guice.support.feature.DummyService; @@ -18,7 +20,7 @@ import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.jupiter.param.Jit; -import javax.inject.Inject; +import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -68,6 +70,8 @@ void checkAllPossibleParams(Application app, ObjectMapper mapper, Injector injector, ClientSupport client, + DropwizardTestSupport support, + ExtensionContext junitContext, DummyService service, @Jit JitService jit) { assertNotNull(app); @@ -78,6 +82,8 @@ void checkAllPossibleParams(Application app, assertNotNull(mapper); assertNotNull(injector); assertNotNull(client); + assertNotNull(support); + assertNotNull(junitContext); assertNotNull(service); assertNotNull(jit); assertEquals(client.getPort(), 8080); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java index daf1010a1..28de4db44 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassDwTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.dw; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassInjectOnceDwTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassInjectOnceDwTest.groovy new file mode 100644 index 000000000..62847d008 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/PerClassInjectOnceDwTest.groovy @@ -0,0 +1,60 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestDropwizardApp(value = App.class, injectOnce = true, debug = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PerClassInjectOnceDwTest { + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // same test instance + Assertions.assertEquals(testId, System.identityHashCode(this)); + // same bean (no second injection - prototype not replaced) + Assertions.assertEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/RandomPortsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/RandomPortsTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/RandomPortsTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/RandomPortsTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppClashDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppClashDwTest.java new file mode 100644 index 000000000..c8233db0e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppClashDwTest.java @@ -0,0 +1,105 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 27.12.2022 + */ +public class ReusableAppClashDwTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "started", "stopped", "stopped"), actions); + Assertions.assertEquals(2, App.cnt); + } + + @TestDropwizardApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + @TestDropwizardApp(value = App.class, randomPorts = true) + public static class Test2 { + + @Test + void testSample(ClientSupport client) { + + // test-own app + Assertions.assertEquals(200, client.targetAdmin("/ping").request().get().getStatus()); + // shared app instance + Assertions.assertEquals(200, client.target("http://localhost:8081/ping").request().get().getStatus()); + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppDwTest.java new file mode 100644 index 000000000..b7f65fd6f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppDwTest.java @@ -0,0 +1,98 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 23.12.2022 + */ +public class ReusableAppDwTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + Assertions.assertEquals(1, App.cnt); + } + + @TestDropwizardApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppManualDwTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppManualDwTest.java new file mode 100644 index 000000000..58d8f22df --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/dw/ReusableAppManualDwTest.java @@ -0,0 +1,103 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.dw; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 23.12.2022 + */ +public class ReusableAppManualDwTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + Assertions.assertEquals(1, App.cnt); + } + + public abstract static class Base { + + @RegisterExtension + static TestDropwizardAppExtension ext = TestDropwizardAppExtension.forApp(App.class) + .reuseApplication() + .create(); + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuicey.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuicey.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuicey.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuicey.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java index 7bf16fe0c..398138f32 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/AnnotatedBaseGuiceyTest.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ApacheClientFactoryGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ApacheClientFactoryGuiceyTest.java new file mode 100644 index 000000000..9a90ced7d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ApacheClientFactoryGuiceyTest.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +@TestGuiceyApp(value = AutoScanApplication.class, apacheClient = true) +public class ApacheClientFactoryGuiceyTest { + + @Test + void testApacheClientConfiguration(ClientSupport client) { + Assertions.assertEquals(Apache5ConnectorProvider.class, + client.getClient().getConfiguration().getConnectorProvider().getClass()); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java similarity index 98% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java index 363aa3083..68e931ab8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ClientSupportGuiceyTest.java @@ -27,6 +27,6 @@ void testLimitedClient(ClientSupport client) { .request().buildGet().invoke().getStatus()); // web methods obviously doesnt work - Assertions.assertThrows(NullPointerException.class, client::basePathMain); + Assertions.assertThrows(NullPointerException.class, client::basePathApp); } } diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyGuiceyTest.java new file mode 100644 index 000000000..a978b7869 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyGuiceyTest.java @@ -0,0 +1,41 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 04.03.2025 + */ +public class ConfigModifyGuiceyTest { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .configOverrides("foo: 2", "bar: 3", "baa: 4") + .configModifiers(config -> { + config.foo = 12; + }) + .create(); + + + @EnableSetup + static TestEnvironmentSetup setup = ext -> + ext.configModifiers(config -> config.bar = 11); + + @Inject + TestConfiguration configuration; + + @Test + void testConfigModification() { + Assertions.assertEquals(12, configuration.foo); + Assertions.assertEquals(11, configuration.bar); + Assertions.assertEquals(4, configuration.baa); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyWithClassGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyWithClassGuiceyTest.java new file mode 100644 index 000000000..e3564d689 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigModifyWithClassGuiceyTest.java @@ -0,0 +1,58 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.server.DefaultServerFactory; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.util.ConfigModifier; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +@TestGuiceyApp(value = AutoScanApplication.class, configModifiers = ConfigModifyWithClassGuiceyTest.FooModifier.class, + configOverride = {"foo: 2", "bar: 3", "baa: 4"}) +public class ConfigModifyWithClassGuiceyTest { + + @EnableSetup + static TestEnvironmentSetup setup = ext -> ext.configModifiers(BarModifier.class, GenericModifier.class); + + @Inject + TestConfiguration configuration; + + @Test + void testConfigModification() { + Assertions.assertEquals(11, configuration.foo); + Assertions.assertEquals(12, configuration.bar); + Assertions.assertEquals(4, configuration.baa); + Assertions.assertEquals(22, ((DefaultServerFactory) configuration.getServerFactory()).getAdminMaxThreads()); + } + + public static class FooModifier implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + config.foo = 11; + } + } + + public static class BarModifier implements ConfigModifier { + @Override + public void modify(TestConfiguration config) throws Exception { + config.bar = 12; + } + } + + public static class GenericModifier implements ConfigModifier { + @Override + public void modify(Configuration config) throws Exception { + ((DefaultServerFactory) config.getServerFactory()).setAdminMaxThreads(22); + ; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideForSetPropertyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideForSetPropertyTest.java new file mode 100644 index 000000000..82699f4a1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideForSetPropertyTest.java @@ -0,0 +1,54 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.configuration.ConfigurationParsingException; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +import java.util.Set; + +/** + * @author Vyacheslav Rusakov + * @since 04.03.2025 + */ +public class ConfigOverrideForSetPropertyTest { + + @Test + void testConfigOverrides() { + + ConfigurationParsingException ex = Assertions.assertThrows(ConfigurationParsingException.class, () -> { + TestSupport.build(App.class) + // IMPOSSIBLE to specify with config override + .configOverride("foo", "['1', '2']") + .runCore(); + }); + + Assertions.assertTrue(ex.getMessage() + .contains("Failed to parse configuration at: foo; Cannot construct instance of `java.util.HashSet` (although at least one Creator exists)")); + } + + public static class Config extends Configuration { + + private Set foo; + + public Set getFoo() { + return foo; + } + } + + public static class App extends Application { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Config configuration, Environment environment) throws Exception { + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java index 29e4e6c04..4a959a590 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideFromExtensionGuiceyTest.java @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; import com.fasterxml.jackson.annotation.JsonProperty; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -14,7 +14,7 @@ import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideGuiceyTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ConfigOverrideGuiceyTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/CustomClientFactoryGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/CustomClientFactoryGuiceyTest.java new file mode 100644 index 000000000..0268dec8b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/CustomClientFactoryGuiceyTest.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.client.CustomTestClientFactory; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 16.11.2023 + */ +@TestGuiceyApp(value = AutoScanApplication.class, clientFactory = CustomTestClientFactory.class) +public class CustomClientFactoryGuiceyTest { + + @Test + void testCustomClientFactory(ClientSupport client) { + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + client.getClient(); // force client creation + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java index 543b0834e..71f782e69 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/GuiceyExtensionShutdownTest.java @@ -1,15 +1,16 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; -import io.dropwizard.Application; -import io.dropwizard.Configuration; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; import io.dropwizard.lifecycle.Managed; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.platform.testkit.engine.EngineTestKit; import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.builder.TestSupportHolder; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; @@ -30,6 +31,7 @@ void checkParallelExecution() { .assertStatistics(stats -> stats.succeeded(1)); Assertions.assertTrue(App.shutdown); + Assertions.assertFalse(TestSupportHolder.isContextSet()); } @TestGuiceyApp(App.class) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java index c0e6eb6bc..c71cd4ca0 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/HooksGuiceyTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.GuiceBundle; @@ -13,7 +13,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigErrorGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigErrorGuiceyTest.java new file mode 100644 index 000000000..d075f0791 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigErrorGuiceyTest.java @@ -0,0 +1,147 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class ManualConfigErrorGuiceyTest extends AbstractPlatformTest { + + @AfterEach + void tearDown() { + SharedConfigurationState.clear(); + ConfigurationHooksSupport.reset(); + } + + @Test + void testConfigPathUsed() { + final Throwable ex = runFailed(Test1.class); + Assertions.assertEquals("Configuration path can't be used with manual configuration instance: /some/path", ex.getMessage()); + } + + @Test + void testConfigOverrideUsed() { + final Throwable ex = runFailed(Test2.class); + Assertions.assertEquals("Configuration overrides can't be used with manual configuration instance: [foo: 1]", ex.getMessage()); + } + + @Test + void testConfigOverrideInstanceUsed() { + final Throwable ex = runFailed(Test3.class); + Assertions.assertEquals("Configuration overrides can't be used with manual configuration instance", ex.getMessage()); + } + + @Test + void testConfigNull() { + final Throwable ex = runFailed(Test4.class); + Assertions.assertEquals("Configuration can't be null", ex.getMessage()); + } + + @Test + void testConfigFail() { + final Throwable ex = runFailed(Test5.class); + Assertions.assertEquals("Manual configuration instance construction failed", ex.getMessage()); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } + + + @Test + void testConfigDuplicate() { + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> { + TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(TestConfiguration::new) + .config(TestConfiguration::new); + }); + Assertions.assertEquals("Manual configuration instance already set", ex.getMessage()); + } + + + @Disabled + public static class Test1 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config("/some/path") + .config(TestConfiguration::new) + .create(); + + @Test + void testConfigPathUsed() { + Assertions.fail(); + } + } + + @Disabled + public static class Test2 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(TestConfiguration::new) + .configOverrides("foo: 1") + .create(); + + @Test + void testConfigOverrideUsed() { + Assertions.fail(); + } + } + + @Disabled + public static class Test3 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(TestConfiguration::new) + .configOverride("foo", () -> "1") + .create(); + + @Test + void testConfigOverrideUsed() { + Assertions.fail(); + } + } + + @Disabled + public static class Test4 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(() -> null) + .create(); + + @Test + void testConfigNull() { + Assertions.fail(); + } + } + + @Disabled + public static class Test5 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(() -> {throw new IllegalStateException("error");}) + .create(); + + @Test + void testConfigFail() { + Assertions.fail(); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigGuiceyTest.java new file mode 100644 index 000000000..ba3499b5c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualConfigGuiceyTest.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class ManualConfigGuiceyTest { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .config(() -> { + TestConfiguration res = new TestConfiguration(); + res.baa = 33; + return res; + }) + .configModifiers(config -> config.foo = 11) + .create(); + + @Inject + TestConfiguration config; + + @Test + void testManualConfiguration() { + Assertions.assertEquals(11, config.foo); + Assertions.assertEquals(33, config.baa); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegApacheClientGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegApacheClientGuiceyTest.java new file mode 100644 index 000000000..076be122a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegApacheClientGuiceyTest.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 12.09.2025 + */ +public class ManualRegApacheClientGuiceyTest { + + @RegisterExtension + static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .apacheClient() + .create(); + + @Test + void testApacheClientRegistration(ClientSupport client) { + Assertions.assertEquals(Apache5ConnectorProvider.class, + client.getClient().getConfiguration().getConnectorProvider().getClass()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegInjectOnceGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegInjectOnceGuiceyTest.java new file mode 100644 index 000000000..34320c773 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegInjectOnceGuiceyTest.java @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import com.google.inject.Inject; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManualRegInjectOnceGuiceyTest { + + @RegisterExtension + TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) + .injectOnce() + .debug() + .create(); + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // same test instance + Assertions.assertEquals(testId, System.identityHashCode(this)); + // same bean (no second injection - prototype not replaced) + Assertions.assertEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java index 1dcf14111..f80bdef59 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ManualRegistrationGuiceyTest.java @@ -9,8 +9,10 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.support.client.CustomTestClientFactory; import ru.vyarus.dropwizard.guice.support.feature.DummyExceptionMapper; import ru.vyarus.dropwizard.guice.support.feature.DummyResource; +import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; /** @@ -25,13 +27,15 @@ public class ManualRegistrationGuiceyTest { .configOverrides("foo: 2", "bar: 12") .hooks(Hook.class) .hooks(builder -> builder.disableExtensions(DummyResource.class)) + // it might seem useless for core test, but client is universal and can be used for external calls + .clientFactory(new CustomTestClientFactory()) .create(); @Inject TestConfiguration config; @Test - void checkCorrectWiring(GuiceyConfigurationInfo info) { + void checkCorrectWiring(GuiceyConfigurationInfo info, ClientSupport client) { Assertions.assertEquals(config.foo, 2); Assertions.assertEquals(config.bar, 12); Assertions.assertEquals(config.baa, 4); @@ -39,6 +43,10 @@ void checkCorrectWiring(GuiceyConfigurationInfo info) { Assertions.assertNotNull(info); Assertions.assertTrue(info.getExtensionsDisabled().contains(DummyResource.class)); Assertions.assertTrue(info.getExtensionsDisabled().contains(DummyExceptionMapper.class)); + + Assertions.assertEquals(0, CustomTestClientFactory.getCalled()); + client.getClient(); // force factory call + Assertions.assertEquals(1, CustomTestClientFactory.getCalled()); } public static class Hook implements GuiceyConfigurationHook { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java index c38d87210..7a5f9ff37 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/MetaAnnotationGuiceyTest.java @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; -import io.dropwizard.Application; +import io.dropwizard.core.Application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleManualTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleManualTest.java new file mode 100644 index 000000000..77ba95543 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleManualTest.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.test.general.BuilderRunCoreWithoutManagedTest; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +public class NoManagedLifecycleManualTest { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(BuilderRunCoreWithoutManagedTest.App.class) + .disableManagedLifecycle() + .create(); + + @Inject + BuilderRunCoreWithoutManagedTest.UnusedManaged managed; + + @Test + void testManagedNotStarted() { + Assertions.assertFalse(managed.started); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleTest.java new file mode 100644 index 000000000..4a740fcd4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/NoManagedLifecycleTest.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.general.BuilderRunCoreWithoutManagedTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@TestGuiceyApp(value = BuilderRunCoreWithoutManagedTest.App.class, managedLifecycle = false) +public class NoManagedLifecycleTest { + + @Inject + BuilderRunCoreWithoutManagedTest.UnusedManaged managed; + + @Test + void testManagedNotStarted() { + Assertions.assertFalse(managed.started); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java similarity index 96% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java index a31da054f..df83ec265 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ObjectConfigOverrideGuiceyTest.java @@ -29,6 +29,7 @@ public class ObjectConfigOverrideGuiceyTest { static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") .configOverrides("foo: 1") + .configOverride("boo", "2") .configOverride("bar", () -> ext.getValue()) .configOverrides(new ConfigOverrideValue("baa", () -> "44")) .create(); @@ -47,6 +48,7 @@ void checkCorrectWiring() { Assertions.assertEquals(config.foo, 1); Assertions.assertEquals(config.bar, 22); Assertions.assertEquals(config.baa, 44); + Assertions.assertEquals(config.boo, 2); } public static class FooExtension implements BeforeAllCallback { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java index 99acd4236..32225c31c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ParametersInjectionGuiceyTest.java @@ -3,14 +3,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; import com.google.inject.Injector; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import ru.vyarus.dropwizard.guice.support.AutoScanApplication; import ru.vyarus.dropwizard.guice.support.TestConfiguration; import ru.vyarus.dropwizard.guice.support.feature.DummyService; @@ -18,7 +20,7 @@ import ru.vyarus.dropwizard.guice.test.ClientSupport; import ru.vyarus.dropwizard.guice.test.jupiter.param.Jit; -import javax.inject.Inject; +import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -67,6 +69,8 @@ void checkAllPossibleParams(Application app, ObjectMapper mapper, Injector injector, ClientSupport clientSupport, + DropwizardTestSupport support, + ExtensionContext junitContext, DummyService service, @Jit JitService jit) { assertNotNull(app); @@ -77,6 +81,8 @@ void checkAllPossibleParams(Application app, assertNotNull(mapper); assertNotNull(injector); assertNotNull(clientSupport); + assertNotNull(support); + assertNotNull(junitContext); assertNotNull(service); assertNotNull(jit); } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java index bc02cbfc8..7296f5e8a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassGuiceyTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.guicey; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; -import javax.inject.Inject; +import jakarta.inject.Inject; /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassInjectOnceGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassInjectOnceGuiceyTest.java new file mode 100644 index 000000000..124b5b842 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassInjectOnceGuiceyTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(value = PerClassInjectOnceGuiceyTest.App.class, injectOnce = true, debug = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerClassInjectOnceGuiceyTest { + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // same test instance + Assertions.assertEquals(testId, System.identityHashCode(this)); + // same bean (no second injection - prototype not replaced) + Assertions.assertEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassNestedInjectOnceTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassNestedInjectOnceTest.java new file mode 100644 index 000000000..c12f777ad --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassNestedInjectOnceTest.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import com.google.inject.Inject; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(value = PerClassNestedInjectOnceTest.App.class, debug = true, injectOnce = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerClassNestedInjectOnceTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + public class Nest { + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // same test instance + Assertions.assertEquals(testId, System.identityHashCode(this)); + // same bean (no second injection - prototype not replaced) + Assertions.assertEquals(beanId, System.identityHashCode(bean)); + } + + } + + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype + public static class Bean {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassPrototypeInjectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassPrototypeInjectionTest.java new file mode 100644 index 000000000..8e8c4d492 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerClassPrototypeInjectionTest.java @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 28.06.2024 + */ +@TestGuiceyApp(PerClassPrototypeInjectionTest.App.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerClassPrototypeInjectionTest { + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + Assertions.assertEquals(testId, System.identityHashCode(this)); + Assertions.assertNotEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerMethodInjectOnceGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerMethodInjectOnceGuiceyTest.java new file mode 100644 index 000000000..bf80c17a2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/PerMethodInjectOnceGuiceyTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * Note: injectOnce USELESS in this case + * + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(value = PerMethodInjectOnceGuiceyTest.App.class, injectOnce = true, debug = true) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) // default +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerMethodInjectOnceGuiceyTest { + + @Inject + Bean bean; + + static int testId; + static int beanId; + + @Test + @Order(1) + void injectTest1() { + Assertions.assertNotNull(bean); + testId = System.identityHashCode(this); + beanId = System.identityHashCode(bean); + } + + @Test + @Order(2) + void injectTest2() { + Assertions.assertNotNull(bean); + // different test instance + Assertions.assertNotEquals(testId, System.identityHashCode(this)); + // ofc, different bean + Assertions.assertNotEquals(beanId, System.identityHashCode(bean)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + // prototype scope + public static class Bean {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppClashGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppClashGuiceyTest.java new file mode 100644 index 000000000..73edef46b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppClashGuiceyTest.java @@ -0,0 +1,99 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 27.12.2022 + */ +public class ReusableAppClashGuiceyTest { + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + err.printStackTrace(); + }); + + Assertions.assertEquals(Arrays.asList("started", "started", "stopped", "stopped"), actions); + Assertions.assertEquals(2, App.cnt); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + @TestGuiceyApp(value = App.class) + public static class Test2 { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppGuiceyTest.java new file mode 100644 index 000000000..1e167e789 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableAppGuiceyTest.java @@ -0,0 +1,98 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 19.12.2022 + */ +public class ReusableAppGuiceyTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + Assertions.assertEquals(1, App.cnt); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableManualAppGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableManualAppGuiceyTest.java new file mode 100644 index 000000000..14319c5fc --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/ReusableManualAppGuiceyTest.java @@ -0,0 +1,102 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 22.12.2022 + */ +public class ReusableManualAppGuiceyTest { + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + Assertions.assertEquals(1, App.cnt); + } + + public abstract static class Base { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .reuseApplication() + .create(); + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/WithBlockGuiceyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/WithBlockGuiceyTest.java new file mode 100644 index 000000000..f574c7de8 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/guicey/WithBlockGuiceyTest.java @@ -0,0 +1,34 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.guicey; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +public class WithBlockGuiceyTest { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(AutoScanApplication.class) + .with(extension -> { + TestConfiguration conf = new TestConfiguration(); + conf.baa = 33; + extension.config(() -> conf); + + }) + .create(); + + @Inject + TestConfiguration config; + + @Test + void testManualConfiguration() { + Assertions.assertEquals(33, config.baa); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclaration2Test.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclaration2Test.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclaration2Test.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclaration2Test.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclarationTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclarationTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/BadHookFieldDeclarationTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/ExceptionRethrowTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/ExceptionRethrowTest.java new file mode 100644 index 000000000..6e936aced --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/ExceptionRethrowTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.hook; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 21.03.2025 + */ +public class ExceptionRethrowTest extends AbstractPlatformTest { + + @Test + void testCheckedException() { + + final Throwable ex = runFailed(Test1.class); + Assertions.assertThat(ex).isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to run hook"); + } + + @Test + void testRuntimeException() { + + final Throwable ex = runFailed(Test2.class); + Assertions.assertThat(ex).isInstanceOf(RuntimeException.class) + .hasMessage("test"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + @EnableHook + static GuiceyConfigurationHook hook = builder -> {throw new IOException("test");}; + + @Test + void test() { + + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + @EnableHook + static GuiceyConfigurationHook hook = builder -> {throw new RuntimeException("test");}; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterBase.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterBase.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterBase.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterBase.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookAfterTest.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookBeforeTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookBeforeTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookBeforeTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookBeforeTest.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookRecognitionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookRecognitionTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookRecognitionTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HookRecognitionTest.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java index 64bffe0a9..8efdb220d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderDeepTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.hook; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,7 +35,7 @@ public static class Middle extends Base { static GuiceyConfigurationHook hook2 = new MiddleHook(); } - @TestGuiceyApp(value = App.class, hooks = TestHook.class) + @TestGuiceyApp(value = App.class, hooks = TestHook.class, useDefaultExtensions = false) @Nested public class TestOrder extends Middle { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java index e1096dfaa..2ab8abe7c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/hook/HooksOrderTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.hook; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -29,7 +29,7 @@ public static class Base { static GuiceyConfigurationHook hook = new BaseHook(); } - @TestGuiceyApp(value = App.class, hooks = TestHook.class) + @TestGuiceyApp(value = App.class, hooks = TestHook.class, useDefaultExtensions = false) @Nested public class TestOrder extends Base { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java index 55e1c8ef7..c209f4025 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodFullAppTest.java @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.test.jupiter.method; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java index 90dbeddde..daedf9eab 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodNestedTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.method; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -11,8 +11,8 @@ import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java index 93e4e8dce..ce48d50cc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodShutdownTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.method; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java similarity index 93% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java index 8a8e780a5..1658fd883 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodSupportFieldsTest.java @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.test.jupiter.method; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java similarity index 84% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java index bf8214a44..9bc5332cc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/method/PerMethodUsageTest.java @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.test.jupiter.method; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import ru.vyarus.dropwizard.guice.GuiceBundle; import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import javax.inject.Inject; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclaration2Test.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclaration2Test.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclaration2Test.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclaration2Test.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclarationTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclarationTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/BadSetupFieldDeclarationTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExceptionsRethrowTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExceptionsRethrowTest.java new file mode 100644 index 000000000..d9a0de461 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExceptionsRethrowTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 21.03.2025 + */ +public class ExceptionsRethrowTest extends AbstractPlatformTest { + + @Test + void testCheckedException() { + + final Throwable ex = runFailed(Test1.class); + Assertions.assertThat(ex).isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to run test setup object"); + } + + @Test + void testRuntimeException() { + + final Throwable ex = runFailed(Test2.class); + Assertions.assertThat(ex).isInstanceOf(RuntimeException.class) + .hasMessage("test"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + @EnableSetup + static TestEnvironmentSetup setup = extension -> {throw new IOException("test");}; + + @Test + void test() { + + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + @EnableSetup + static TestEnvironmentSetup setup = extension -> {throw new RuntimeException("test");}; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExtensionsDisableTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExtensionsDisableTest.groovy new file mode 100644 index 000000000..420e5b71e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ExtensionsDisableTest.groovy @@ -0,0 +1,96 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import ru.vyarus.dropwizard.guice.AbstractPlatformTest +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ + +class ExtensionsDisableTest extends AbstractPlatformTest { + + @Test + void testGuiceyAnn() { + + String res = run(TestGuiceyAnn) + Assertions.assertThat(res).doesNotContain("StubsSupport") + } + + @Test + void testDwAnn() { + + String res = run(TestDwAnn) + Assertions.assertThat(res).doesNotContain("StubsSupport") + } + + @Test + void testGuiceyExt() { + + String res = run(TestGuiceyExt) + Assertions.assertThat(res).doesNotContain("StubsSupport") + } + + @Test + void testDwExt() { + + String res = run(TestDwExt) + Assertions.assertThat(res).doesNotContain("StubsSupport") + } + + @TestGuiceyApp(value = DefaultTestApp, useDefaultExtensions = false, debug = true) + @Disabled + static class TestGuiceyAnn { + + @Test + void test() { + // nothing + } + } + + @TestDropwizardApp(value = DefaultTestApp, useDefaultExtensions = false, debug = true) + @Disabled + static class TestDwAnn { + + @Test + void test() { + // nothing + } + } + + @Disabled + static class TestGuiceyExt { + + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp) + .disableDefaultExtensions().debug().create() + + @Test + void test() { + // nothing + } + } + + @Disabled + static class TestDwExt { + + static TestDropwizardAppExtension ext = TestDropwizardAppExtension.forApp(DefaultTestApp) + .disableDefaultExtensions().debug().create() + + @Test + void test() { + // nothing + } + } + + @Override + protected String clean(String out) { + return out + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/FieldsSearchTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/FieldsSearchTest.java new file mode 100644 index 000000000..c5140e3f0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/FieldsSearchTest.java @@ -0,0 +1,198 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.TestFieldUtils; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +public class FieldsSearchTest { + + @Test + void testAnnotatedFieldsSearch() { + + final List> fields = TestFieldUtils + .findAnnotatedFields(Root.class, MockBean.class, Object.class); + + Assertions.assertEquals(6, fields.size()); + + final List> own = TestFieldUtils.getTestOwnFields(fields); + Assertions.assertEquals(3, own.size()); + Assertions.assertEquals(0, own.stream() + .filter(field -> field.getDeclaringClass() == Base.class).count()); + + final List> base = TestFieldUtils.getInheritedFields(fields); + Assertions.assertEquals(3, base.size()); + Assertions.assertEquals(0, base.stream() + .filter(field -> field.getDeclaringClass() == Root.class).count()); + } + + @Test + void testBaseTypeValidation() { + + try { + TestFieldUtils.findAnnotatedFields(Root.class, MockBean.class, Base.class); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock1 annotated with @MockBean, but its type is not Base", e.getMessage()); + } + } + + @Test + void testFieldValidations() { + final List> fields = TestFieldUtils + .findAnnotatedFields(Base.class, MockBean.class, Object.class); + + AnnotatedField bmock1 = fields.stream() + .filter(field -> field.getName().equals("bmock1")) + .findFirst().get(); + + Assertions.assertEquals("FieldsSearchTest$Base.bmock1 (@MockBean static Service)", bmock1.toString()); + + bmock1.requireStatic(); + + try { + bmock1.requireNonStatic(); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock1 annotated with @MockBean, must not be static", e.getMessage()); + } + + + AnnotatedField bmock2 = fields.stream() + .filter(field -> field.getName().equals("bmock2")) + .findFirst().get(); + + Assertions.assertEquals("FieldsSearchTest$Base.bmock2 (@MockBean Service)", bmock2.toString()); + + bmock2.requireNonStatic(); + + try { + bmock2.requireStatic(); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock2 annotated with @MockBean, must be static", e.getMessage()); + } + } + + @Test + void testStaticValueValidation() { + final List> fields = TestFieldUtils + .findAnnotatedFields(Base.class, MockBean.class, Object.class); + + AnnotatedField bmock1 = fields.stream() + .filter(field -> field.getName().equals("bmock1")) + .findFirst().get(); + + bmock1.setValue(null, new Service()); + // instance ignored + bmock1.setValue(new Base(), new Service()); + bmock1.setValue(new Object(), new Service()); + + Assertions.assertNotNull(bmock1.getValue(null)); + Assertions.assertNotNull(bmock1.getValue(new Object())); + + try { + bmock1.setValue(null, new Object()); + Assertions.fail(); + } catch (IllegalArgumentException e) { + Assertions.assertEquals("Can not set static ru.vyarus.dropwizard.guice.test.jupiter.setup.FieldsSearchTest$Service field ru.vyarus.dropwizard.guice.test.jupiter.setup.FieldsSearchTest$Base.bmock1 to java.lang.Object", e.getMessage()); + } + + } + + @Test + void testNonStaticValueValidation() { + final List> fields = TestFieldUtils + .findAnnotatedFields(Base.class, MockBean.class, Object.class); + + AnnotatedField bmock2 = fields.stream() + .filter(field -> field.getName().equals("bmock2")) + .findFirst().get(); + + try { + bmock2.setValue(null, new Service()); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock2 is not static: test instance required for setting value", e.getMessage()); + } + + bmock2.setValue(new Base(), new Service()); + + try { + bmock2.setValue(new Object(), new Service()); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Invalid instance provided: class java.lang.Object for field FieldsSearchTest$Base.bmock2", e.getMessage()); + } + + try { + Assertions.assertNotNull(bmock2.getValue(null)); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock2 is not static: test instance required for obtaining value", e.getMessage()); + } + + try { + Assertions.assertNotNull(bmock2.getValue(new Object())); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Invalid instance provided: class java.lang.Object for field FieldsSearchTest$Base.bmock2", e.getMessage()); + } + } + + @Test + void testFiledValueChangeDetection() throws Exception { + final List> fields = TestFieldUtils + .findAnnotatedFields(Base.class, MockBean.class, Object.class); + + AnnotatedField bmock1 = fields.stream() + .filter(field -> field.getName().equals("bmock1")) + .findFirst().get(); + + bmock1.setValue(null, new Service()); + + // manual override + bmock1.getField().set(null, new Service()); + + try { + bmock1.checkValueNotChanged(null); + Assertions.fail(); + } catch (IllegalStateException e) { + Assertions.assertEquals("Field FieldsSearchTest$Base.bmock1 annotated with @MockBean value was changed: " + + "most likely, it happen in test setup method, which is called after Injector startup and so too late to change " + + "binding values. Manual initialization is possible in field directly.", e.getMessage()); + } + } + + public static class Base { + + @MockBean + static Service bmock1; + + @MockBean + Service bmock2; + + @MockBean + private static Service bmock3; + } + + public static class Root extends Base { + @MockBean + static Service mock1; + + @MockBean + Service mock2; + + @MockBean + private static Service mock3; + } + + public static class Service {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/JunitContextAccessTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/JunitContextAccessTest.groovy new file mode 100644 index 000000000..d7efa86c0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/JunitContextAccessTest.groovy @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup + +import com.google.inject.AbstractModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import jakarta.inject.Inject +import org.junit.jupiter.api.extension.ExtensionContext +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2025 + */ +@TestGuiceyApp(App) +class JunitContextAccessTest extends AbstractTest { + + @EnableSetup + static TestEnvironmentSetup setup = new TestEnvironmentSetup() { + @Override + Object setup(TestExtension extension) { + extension.hooks(new GuiceyConfigurationHook() { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new AbstractModule() { + @Override + protected void configure() { + bind(ExtensionContext).toInstance(extension.getJunitContext()) + } + }) + } + }) + return null + } + } + + @Inject + ExtensionContext context + + def "Check junit context accessible"() { + + expect: "context provided" + context != null + context.getRequiredTestClass() == JunitContextAccessTest + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java new file mode 100644 index 000000000..1f0d26ae9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerLambdaTest.java @@ -0,0 +1,156 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +public class ListenerLambdaTest { + public static List actions = new ArrayList<>(); + + + @Test + void checkListeners() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeAll", "beforeEach", "afterEach", "beforeEach", "afterEach", "stopped", "afterAll"), actions); + } + + + @Test + void checkListenersForPerMethod() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeEach", "stopped", "afterEach", "started", "beforeEach", "stopped", "afterEach"), actions); + } + + @TestGuiceyApp(value = DefaultTestApp.class, debug = true, setup = Setup.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + @Disabled // prevent direct execution + public static class Test2 { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp.class) + .setup(Setup.class) + .debug() + .create(); + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + public static class Setup implements TestEnvironmentSetup { + @Override + public Object setup(TestExtension extension) { + return extension + .onApplicationStart(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + Preconditions.checkNotNull(context.getBean(GuiceyConfigurationInfo.class).getActiveScopes()); + actions.add("started"); + }) + .onBeforeAll(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeAll"); + }) + .onBeforeEach(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeEach"); + }) + .onAfterEach(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterEach"); + }) + .onAfterAll(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterAll"); + }) + .onApplicationStop(context -> { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("stopped"); + }); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java new file mode 100644 index 000000000..58fa75508 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ListenerTest.java @@ -0,0 +1,189 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import com.google.common.base.Preconditions; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +public class ListenerTest { + public static List actions = new ArrayList<>(); + + + @Test + void checkListeners() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeAll", "beforeEach", "afterEach", "beforeEach", "afterEach", "stopped", "afterAll"), actions); + } + + + @Test + void checkListenersForPerMethod() { + actions.clear(); + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "beforeEach", "stopped", "afterEach", "started", "beforeEach", "stopped", "afterEach"), actions); + } + + + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @TestGuiceyApp(value = App.class, debug = true, setup = Setup.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + @Disabled // prevent direct execution + public static class Test2 { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .setup(Setup.class) + .debug() + .create(); + + @Test + void fooTest() { + } + + @Test + void fooTest2() { + } + } + + public static class Setup implements TestEnvironmentSetup { + @Override + public Object setup(TestExtension extension) { + extension.listen(new TestExecutionListener() { + @Override + public void started(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + Preconditions.checkNotNull(context.getBean(GuiceyConfigurationInfo.class).getActiveScopes()); + actions.add("started"); + } + + @Override + public void beforeAll(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeAll"); + } + + @Override + public void beforeEach(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("beforeEach"); + } + + @Override + public void afterEach(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterEach"); + } + + @Override + public void afterAll(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("afterAll"); + } + + @Override + public void stopped(EventContext context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getJunitContext()); + Preconditions.checkNotNull(context.getSupport()); + Preconditions.checkNotNull(context.getInjector()); + Preconditions.checkNotNull(context.getClient()); + actions.add("stopped"); + } + }); + return null; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ManualConfigTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ManualConfigTest.java new file mode 100644 index 000000000..ea357357b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/ManualConfigTest.java @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +@TestGuiceyApp(AutoScanApplication.class) +public class ManualConfigTest { + + @EnableSetup + static TestEnvironmentSetup setup = ext -> ext.config(() -> { + TestConfiguration conf = new TestConfiguration(); + conf.foo = 12; + return conf; + }); + + @Inject + TestConfiguration config; + + @Test + void testManualConfigFromSetup() { + Assertions.assertEquals(12, config.foo); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java index bea8fae35..2befab117 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupAndHookInOneFieldTest.java @@ -19,9 +19,12 @@ * @author Vyacheslav Rusakov * @since 18.05.2022 */ -@TestGuiceyApp(AutoScanApplication.class) +@TestGuiceyApp(value = AutoScanApplication.class, debug = true) public class SetupAndHookInOneFieldTest { + // NOTE: in debug hook would appear two times: 1 from auto registration from setup and one from field + // (setup objects processed before class hook fields) + // Still, hook would be called only once @EnableHook @EnableSetup static Hook hook = new Hook(); diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupClosingTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupClosingTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupClosingTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupClosingTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java new file mode 100644 index 000000000..657acf894 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupObjectAutoRegistrationTest.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.EventContext; +import ru.vyarus.dropwizard.guice.test.jupiter.env.listen.TestExecutionListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +@TestGuiceyApp(value = SetupObjectAutoRegistrationTest.App.class, + setup = SetupObjectAutoRegistrationTest.SetupObject.class, + debug = true) +public class SetupObjectAutoRegistrationTest { + + static List actions = new ArrayList<>(); + + @Test + void testDuplicateRegistration() { + + Assertions.assertEquals(Arrays.asList("setup", "configure", "beforeEach"), actions); + } + + public static class App extends Application { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + public static class SetupObject implements TestEnvironmentSetup, GuiceyConfigurationHook, TestExecutionListener { + + @Override + public Object setup(TestExtension extension) { + actions.add("setup"); + // not required manual registration + return extension.hooks(this).listen(this); + } + + + @Override + public void configure(GuiceBundle.Builder builder) { + actions.add("configure"); + } + + @Override + public void beforeEach(EventContext context) { + actions.add("beforeEach"); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition2Test.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition2Test.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition2Test.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition2Test.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition3Test.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition3Test.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition3Test.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognition3Test.java diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognitionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognitionTest.java similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognitionTest.java rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/SetupRecognitionTest.java diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/WithBlockTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/WithBlockTest.java new file mode 100644 index 000000000..9f5908fd4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/WithBlockTest.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.AutoScanApplication; +import ru.vyarus.dropwizard.guice.support.TestConfiguration; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; + +/** + * @author Vyacheslav Rusakov + * @since 05.03.2025 + */ +@TestGuiceyApp(AutoScanApplication.class) +public class WithBlockTest { + + @EnableSetup + static TestEnvironmentSetup setup = ext -> ext + .with(extension -> { + // just an example! for config better to use #config directly + TestConfiguration conf = new TestConfiguration(); + conf.foo = 12; + extension.config(() -> conf); + }); + + @Inject + TestConfiguration cfg; + + @Test + void testWithBlock() { + Assertions.assertEquals(12, cfg.foo); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientInjectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientInjectionTest.java new file mode 100644 index 000000000..0db915181 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientInjectionTest.java @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client; + +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClientType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +@TestDropwizardApp(value = DefaultTestApp.class, configOverride = { + "server.applicationContextPath: /app", + "server.adminContextPath: /admin", +}, restMapping = "api") +public class ClientInjectionTest { + + @WebClient + ClientSupport client; + + @WebClient(WebClientType.App) + TestClient app; + + @WebClient(WebClientType.Admin) + TestClient admin; + + @WebClient(WebClientType.Rest) + TestClient rest; + + @Test + void testClientInjection(ClientSupport support) { + assertThat(app).isNotNull(); + assertThat(admin).isNotNull(); + assertThat(rest).isNotNull(); + + assertThat(client).isNotNull().isSameAs(support); + assertThat(app.getBaseUri()).isEqualTo(support.appClient().getBaseUri()); + assertThat(admin.getBaseUri()).isEqualTo(support.adminClient().getBaseUri()); + assertThat(rest.getBaseUri()).isEqualTo(support.restClient().getBaseUri()); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientResetTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientResetTest.java new file mode 100644 index 000000000..900caf923 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/ClientResetTest.java @@ -0,0 +1,67 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class ClientResetTest extends AbstractPlatformTest { + + @Test + void testDefaultsReset() { + runSuccess(Test1.class, Test2.class); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + @WebClient + static ClientSupport client; + + @Test + void test1() { + client.defaultLanguage(Locale.ENGLISH); + } + + @AfterAll + static void afterAll() { + assertThat(client.hasDefaults()).isFalse(); + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + + @WebClient(autoReset = false) + static ClientSupport client; + + @Test + void test1() { + client.defaultLanguage(Locale.ENGLISH); + } + + @AfterAll + static void afterAll() { + assertThat(client.hasDefaults()).isTrue(); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/FailedClientInjectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/FailedClientInjectionTest.java new file mode 100644 index 000000000..468c07a67 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/FailedClientInjectionTest.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class FailedClientInjectionTest extends AbstractPlatformTest { + + @Test + void testClientTypeValidation() { + Throwable ex = runFailed(Test1.class); + assertThat(ex.getMessage()).isEqualTo("ClientSupport type must be used for the default @WebClient field: FailedClientInjectionTest$Test1.client"); + } + + @Disabled + @TestGuiceyApp(DefaultTestApp.class) + public static class Test1 { + + @WebClient + TestClient client; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/GuiceyTestClientInjectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/GuiceyTestClientInjectionTest.java new file mode 100644 index 000000000..7c0ea10a9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/GuiceyTestClientInjectionTest.java @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.WebClient; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class GuiceyTestClientInjectionTest { + + @WebClient + ClientSupport client; + + @Test + void testClientInjected(ClientSupport client) { + Assertions.assertThat(client).isNotNull().isSameAs(this.client); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestApp.java new file mode 100644 index 000000000..817638017 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestApp.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client.rest; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class RestApp extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(Resource.class) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + + @Path("/resource") + public static class Resource { + + @GET + public String get() { + return "ok"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestClientResetTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestClientResetTest.java new file mode 100644 index 000000000..57eb84c95 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/RestClientResetTest.java @@ -0,0 +1,67 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client.rest; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class RestClientResetTest extends AbstractPlatformTest { + + @Test + void testDefaultsReset() { + runSuccess(Test1.class, Test2.class); + } + + @TestDropwizardApp(RestApp.class) + @Disabled + public static class Test1 { + + @WebResourceClient + static ResourceClient client; + + @Test + void test1() { + client.defaultLanguage(Locale.ENGLISH); + } + + @AfterAll + static void afterAll() { + assertThat(client.hasDefaults()).isFalse(); + } + } + + @TestDropwizardApp(RestApp.class) + @Disabled + public static class Test2 { + + @WebResourceClient(autoReset = false) + static ResourceClient client; + + @Test + void test1() { + client.defaultLanguage(Locale.ENGLISH); + } + + @AfterAll + static void afterAll() { + assertThat(client.hasDefaults()).isTrue(); + } + } + + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientErrorsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientErrorsTest.java new file mode 100644 index 000000000..f5b86216d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientErrorsTest.java @@ -0,0 +1,80 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.client.TestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +public class WebResourceClientErrorsTest extends AbstractPlatformTest { + + @Test + void testNoRestStubsError() { + Assertions.assertThat(runFailed(Test1.class).getMessage()) + .isEqualTo("Resource client can't be used under lightweight guicey test without @StubRest: WebResourceClientErrorsTest$Test1.rest"); + } + + @Test + void testBadResource() { + Assertions.assertThat(runFailed(Test2.class).getMessage()) + .isEqualTo("Target resource class must be specified as generic (ResourceClient) in field: WebResourceClientErrorsTest$Test2.rest"); + } + + @Test + void testBadType() { + Assertions.assertThat(runFailed(Test3.class).getMessage()) + .isEqualTo("Field WebResourceClientErrorsTest$Test3.rest annotated with @WebResourceClient, but its type is not ResourceClient"); + } + + @TestGuiceyApp(RestApp.class) + @Disabled + public static class Test1 { + + @WebResourceClient + ResourceClient rest; + + @Test + void test() { + + } + } + + + @TestGuiceyApp(RestApp.class) + @Disabled + public static class Test2 { + + @WebResourceClient + ResourceClient rest; + + @Test + void test() { + + } + } + + @TestGuiceyApp(RestApp.class) + @Disabled + public static class Test3 { + + @WebResourceClient + TestClient rest; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientStubsTest.java new file mode 100644 index 000000000..b104d7331 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientStubsTest.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +@TestGuiceyApp(RestApp.class) +public class WebResourceClientStubsTest { + + @StubRest + RestClient client; + + @WebResourceClient + ResourceClient rest; + + @Test + void testRestClient() { + Assertions.assertThat(rest.method(RestApp.Resource::get).as(String.class)).isEqualTo("ok"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientTest.java new file mode 100644 index 000000000..557a26433 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/client/rest/WebResourceClientTest.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.client.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.client.rest.WebResourceClient; + +/** + * @author Vyacheslav Rusakov + * @since 17.10.2025 + */ +@TestDropwizardApp(RestApp.class) +public class WebResourceClientTest { + + @WebResourceClient + ResourceClient rest; + + @Test + void testRestClient() { + Assertions.assertThat(rest.method(RestApp.Resource::get).as(String.class)).isEqualTo("ok"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsCleanupTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsCleanupTest.java new file mode 100644 index 000000000..7a4d810c4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsCleanupTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class LogsCleanupTest extends AbstractPlatformTest { + + @Test + void testAutoReset() { + runSuccess(Test1.class, Test2.class); + } + + @TestGuiceyApp(LogRecordsApp.class) + @Disabled + public static class Test1 { + + @RecordLogs(loggers = "ru.vyarus.dropwizard.guice.test.log.support", level = Level.TRACE) + static RecordedLogs logs; + + @Test + void test() { + Assertions.assertFalse(logs.empty()); + } + + @AfterAll + static void afterAll() { + Assertions.assertTrue(logs.empty()); + } + } + + @TestGuiceyApp(LogRecordsApp.class) + @Disabled + public static class Test2 { + + @RecordLogs(loggers = "ru.vyarus.dropwizard.guice.test.log.support", + level = Level.TRACE, autoReset = false) + static RecordedLogs logs; + + @Test + void test() { + Assertions.assertFalse(logs.empty()); + } + + @AfterAll + static void afterAll() { + Assertions.assertFalse(logs.empty()); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordCompletenessTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordCompletenessTest.java new file mode 100644 index 000000000..17074d71f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordCompletenessTest.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; +import ru.vyarus.dropwizard.guice.test.log.support.DBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.DBundleAfter; +import ru.vyarus.dropwizard.guice.test.log.support.DBundleBefore; +import ru.vyarus.dropwizard.guice.test.log.support.DManaged; +import ru.vyarus.dropwizard.guice.test.log.support.GBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.GModule; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +@TestGuiceyApp(LogRecordsApp.class) +public class LogsRecordCompletenessTest { + + @RecordLogs(loggers = "ru.vyarus.dropwizard.guice.test.log.support", level = Level.TRACE) + RecordedLogs logs; + + @Test + void testStartupLogsRecorded() { + Assertions.assertEquals(13, logs.count()); + Assertions.assertEquals(13, logs.level(Level.TRACE).count()); + + // impossible to detect run event (bundle runs after loggers reset and before guicey run) + Assertions.assertEquals(Arrays.asList("Bundle initialized"), + logs.logger(DBundleBefore.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(DBundle1.class).messages()); + Assertions.assertEquals(logs.logger(DBundle1.class).messages().size(), + logs.logger(DBundle1.class).events().size()); + Assertions.assertEquals(logs.logger(DBundle1.class).level(Level.TRACE).count(), + logs.logger(DBundle1.class).level(Level.TRACE).count()); + Assertions.assertTrue(logs.logger(DBundle1.class).has(Level.TRACE)); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(GBundle1.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(DBundleAfter.class).messages()); + + Assertions.assertEquals(Arrays.asList("Managed started"), + logs.logger(DManaged.class).messages()); + + Assertions.assertEquals(Arrays.asList("Module configured"), + logs.logger(GModule.class).messages()); + + Assertions.assertEquals(Arrays.asList("Constructor", "Before init", "After init", "Run"), + logs.logger(LogRecordsApp.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle started", "Bundle started", "Bundle started", + "Managed started"), logs.containing("started").messages()); + + Assertions.assertEquals(Arrays.asList("Bundle started"), + logs.logger(DBundle1.class).containing("started").messages()); + Assertions.assertEquals(Arrays.asList("Bundle started"), + logs.logger(DBundle1.class).matching("Bund.+ started").messages()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSimpleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSimpleTest.java new file mode 100644 index 000000000..5e63957e0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSimpleTest.java @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log; + +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; + +/** + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +public class LogsRecordSimpleTest { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.extensions(Service.class); + + @RecordLogs(value = Service.class, level = Level.DEBUG) + RecordedLogs logs; + + @Inject + Service service; + + @Test + void test() { + + Assertions.assertNotNull(logs); + Assertions.assertEquals(2, logs.count()); + Assertions.assertEquals(logs.events().size(), logs.messages().size()); + Assertions.assertEquals(2, logs.logger(Service.class).count()); + Assertions.assertTrue(logs.has(Level.DEBUG)); + Assertions.assertTrue(logs.has(Level.INFO)); + Assertions.assertEquals(1, logs.level(Level.DEBUG).count()); + Assertions.assertEquals(1, logs.level(Level.INFO).count()); + + service.foo(); + Assertions.assertEquals(3, logs.count()); + Assertions.assertTrue(logs.has(Level.WARN)); + Assertions.assertEquals(1, logs.level(Level.WARN).count()); + Assertions.assertEquals("Foo called", logs.lastMessage()); + } + + @Singleton + public static class Service implements Managed { + private final Logger logger = LoggerFactory.getLogger(Service.class); + + public Service() { + logger.debug("Created Service {}", "smth"); + } + + @Override + public void start() throws Exception { + logger.info("Start"); + } + + @Override + public void stop() throws Exception { + logger.info("Stop"); + } + + public void foo() { + logger.warn("Foo called"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSpockTest.groovy new file mode 100644 index 000000000..096e944d7 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsRecordSpockTest.groovy @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log + +import io.dropwizard.lifecycle.Managed +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.event.Level +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.EnableHook +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = DefaultTestApp, debug = true) +class LogsRecordSpockTest extends Specification { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.extensions(Service) + + @RecordLogs(value = Service, level = Level.DEBUG) + RecordedLogs logs + + @Inject + Service service + + def "Check log records"() { + + expect: + logs + 2 == logs.count() + + when: + service.foo() + + then: + 3 == logs.count() + logs.has(Level.WARN) + 1 == logs.level(Level.WARN).count() + } + + @Singleton + static class Service implements Managed { + private final Logger logger = LoggerFactory.getLogger(Service) + + Service() { + logger.debug("Created Service {}", "smth") + } + + @Override + void start() throws Exception { + logger.info("Start") + } + + @Override + void stop() throws Exception { + logger.info("Stop") + } + + void foo() { + logger.warn("Foo called") + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsReportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsReportTest.java new file mode 100644 index 000000000..851b0cd77 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/LogsReportTest.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class LogsReportTest extends AbstractPlatformTest { + + @Test + void testDebugReport() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("Applied log recorders (@RecordLogs) on LogsReportTest$Test1\n" + + "\n" + + "\t#logs WARN "); + } + + @TestGuiceyApp(value = LogRecordsApp.class, debug = true) + @Disabled + public static class Test1 { + + @RecordLogs + RecordedLogs logs; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/NotRiseLevelTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/NotRiseLevelTest.java new file mode 100644 index 000000000..8df62be8b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/log/NotRiseLevelTest.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.log; + +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordLogs; +import ru.vyarus.dropwizard.guice.test.log.RecordedLogs; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +@TestGuiceyApp(LogRecordsApp.class) +public class NotRiseLevelTest { + + @RecordLogs(level = Level.ERROR) + RecordedLogs logs; + + @Test + void testLoggerLevelNotChanged() { + Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + Assertions.assertEquals(ch.qos.logback.classic.Level.INFO, logger.getLevel()); + Assertions.assertEquals(0, logs.count()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/AbstractMockTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/AbstractMockTest.java new file mode 100644 index 000000000..fdc31878b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/AbstractMockTest.java @@ -0,0 +1,18 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; + +/** + * @author Vyacheslav Rusakov + * @since 18.02.2025 + */ +public abstract class AbstractMockTest extends AbstractPlatformTest { + + @Override + protected String clean(String out) { + return out + .replaceAll("@[\\da-z]{6,10}", "@11111111") + .replaceAll("hashCode: \\d+", "hashCode: 11111111") + .replaceAll("\\d+(\\.\\d+) ms", "11.11 ms"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/IncorrectMockDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/IncorrectMockDeclarationTest.java new file mode 100644 index 000000000..bc26dca2d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/IncorrectMockDeclarationTest.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +public class IncorrectMockDeclarationTest extends AbstractMockTest { + + @Test + void testIncorrectManualMockDeclaration() { + + Throwable th = runFailed(Test1.class); + + Assertions.assertThat(th.getMessage()).contains( + "Incorrect @MockBean 'IncorrectMockDeclarationTest$Test1.serviceMock' declaration: initialized " + + "instance is not a mockito mock object. Either provide correct mock or remove value and let " + + "extension create mock automatically."); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + static Service mock = new Service(); + + @MockBean + static Service serviceMock = mock; + + @Test + void testManualMock() { + Assertions.assertThat(serviceMock).isSameAs(mock); + } + + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/InstanceMockTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/InstanceMockTest.java new file mode 100644 index 000000000..e1a8ec12e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/InstanceMockTest.java @@ -0,0 +1,52 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +@TestGuiceyApp(InstanceMockTest.App.class) +public class InstanceMockTest { + + @Inject + Service service; + + @MockBean + Service mock; + + @BeforeEach + void setUp() { + Mockito.when(mock.foo()).thenReturn("bar"); + } + + @Test + void testMockedInstance() { + Assertions.assertThat(service).isEqualTo(mock); + Assertions.assertThat(service.foo()).isEqualTo("bar"); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + // mocks override existing binding and so instances could be mocked + .modules(binder -> binder.bind(Service.class).toInstance(new Service())) + .build(); + } + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/ManualMockTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/ManualMockTest.java new file mode 100644 index 000000000..a8dc710fb --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/ManualMockTest.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +public class ManualMockTest { + + static Service mock = Mockito.mock(Service.class); + + @MockBean + static Service serviceMock = mock; + + @Test + void testManualMock() { + Assertions.assertThat(serviceMock).isSameAs(mock); + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockCleanupDisableTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockCleanupDisableTest.java new file mode 100644 index 000000000..9799e4f50 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockCleanupDisableTest.java @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class MockCleanupDisableTest { + + @MockBean(autoReset = false) + static Service mock; + + @Test + void testMethod() { + Mockito.when(mock.foo()).thenReturn("bar"); + } + + @AfterAll + static void afterAll() { + // mock auto cleaned after each test - without cleanup, here will be overridden method + Assertions.assertThat(mock.foo()).isEqualTo("bar"); + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSimpleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSimpleTest.java new file mode 100644 index 000000000..87eadc1dc --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSimpleTest.java @@ -0,0 +1,79 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MockSimpleTest { + + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @MockBean + Service1 mock1; + + @MockBean + static Service2 mock2; + + @BeforeAll + static void beforeAll() { + Preconditions.checkNotNull(mock2); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(mock1); + Preconditions.checkNotNull(mock2); + + Mockito.when(mock1.foo()).thenReturn("bar1"); + Mockito.when(mock2.foo()).thenReturn("bar2"); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, mock1); + Assertions.assertEquals(service2, mock2); + Assertions.assertEquals("bar1", service1.foo()); + Assertions.assertEquals("bar2", service2.foo()); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, mock1); + Assertions.assertEquals(service2, mock2); + Assertions.assertEquals("bar1", service1.foo()); + } + + public static class Service1 { + public String foo() { + return "foo1"; + } + } + + public static class Service2 { + public String foo() { + return "foo2"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSummaryTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSummaryTest.java new file mode 100644 index 000000000..0fba2ae74 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MockSummaryTest.java @@ -0,0 +1,77 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 18.02.2025 + */ +public class MockSummaryTest extends AbstractMockTest { + + @Test + void testMockSummary() { + + String out = run(Test1.class); + + Assertions.assertThat(out).contains("@MockBean stats on [After each] for MockSummaryTest$Test1#test():\n" + + "\n" + + "\t[Mockito] Interactions of: Mock for Service, hashCode: 11111111\n" + + "\t 1. service.foo(1);\n" + + "\t -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.test(MockSummaryTest.java:55)\n" + + "\t - stubbed -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.setUp(MockSummaryTest.java:50)"); + } + + @Test + void testNoMockSummary() { + + String out = run(Test2.class); + + Assertions.assertThat(out).contains("@MockBean stats on [After each] for MockSummaryTest$Test2#test():\n" + + "\n" + + "\tNo interactions and stubbings found for mock: Mock for Service, hashCode: 11111111"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + @MockBean(printSummary = true) + Service service; + + @BeforeEach + void setUp() { + Mockito.when(service.foo(Mockito.anyInt())).thenReturn("bar"); + } + + @Test + void test() { + Assertions.assertThat(service.foo(1)).isEqualTo("bar"); + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + + @MockBean(printSummary = true) + Service service; + + @Test + void test() { + // mock not used + } + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksResetTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksResetTest.java new file mode 100644 index 000000000..bf3d81b95 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksResetTest.java @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MocksResetTest { + + @MockBean + Service mock; + + @BeforeEach + void setUp() { + Mockito.when(mock.foo()).thenReturn("bar"); + } + + @Test + void test1() { + mock.foo(); + Mockito.verify(mock).foo(); + } + + @Test + void test2() { + mock.foo(); + // no second exec (put autoReset = false to make sure) + Mockito.verify(mock).foo(); + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksSpockTest.groovy new file mode 100644 index 000000000..6d6623764 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/mock/MocksSpockTest.groovy @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.mock + + +import com.google.inject.Inject +import org.mockito.Mockito +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = DefaultTestApp, debug = true) +class MocksSpockTest extends AbstractTest { + + @Inject + Service1 service1 + + @Inject + Service2 service2 + + @MockBean + Service1 mock1 + + @MockBean + static Service2 mock2 + + void setupSpec() { + assert mock2 + } + + void setup() { + assert mock1 + assert mock2 + + Mockito.when(mock1.foo()).thenReturn("bar1") + Mockito.when(mock2.foo()).thenReturn("bar2") + } + + def "Check mock execution"() { + + expect: + service1 == mock1 + service2 == mock2 + "bar1" == service1.foo() + "bar2" == service2.foo() + } + + static class Service1 { + String foo() { + return "foo1" + } + } + + static class Service2 { + String foo() { + return "foo2" + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ClientDefaultsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ClientDefaultsTest.java new file mode 100644 index 000000000..5450948a0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ClientDefaultsTest.java @@ -0,0 +1,58 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import jakarta.ws.rs.core.MediaType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class ClientDefaultsTest extends AbstractPlatformTest { + + @Test + void testDefaults() { + String out = run(Test1.class); + Assertions.assertThat(out).contains("[Client action]---------------------------------------------{\n" + + "1 * Sending client request on thread ddd\n" + + "1 > GET http://localhost:0/1/foo?par1=val1&par2=val2\n" + + "1 > Accept: application/json\n" + + "1 > Boo: baz\n" + + "1 > Foo: bar\n" + + "\n" + + "}----------------------------------------------------------"); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @StubRest + RestClient rest; + + @Test + void test() { + rest.defaultAccept(MediaType.APPLICATION_JSON); + rest.defaultHeader("Foo", "bar"); + rest.defaultHeader("Boo", "baz"); + rest.defaultQueryParam("par1", "val1"); + rest.defaultQueryParam("par2", "val2"); + rest.get("/1/foo", String.class); + + org.junit.jupiter.api.Assertions.assertTrue(rest.hasDefaultAccepts()); + org.junit.jupiter.api.Assertions.assertTrue(rest.hasDefaultHeaders()); + org.junit.jupiter.api.Assertions.assertTrue(rest.hasDefaultQueryParams()); + } + } + + @Override + protected String clean(String out) { + return out.replaceAll("on thread ([^\n]+)", "on thread ddd"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ContainerSelectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ContainerSelectionTest.java new file mode 100644 index 000000000..9dd377f43 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ContainerSelectionTest.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.TestContainerPolicy; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class ContainerSelectionTest extends AbstractPlatformTest { + + @Test + void testContainerSelection() { + final Throwable ex = runFailed(Test1.class); + Assertions.assertEquals("org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory is not available in classpath. " + + "Add `org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2` " + + "dependency (version managed by dropwizard BOM)", ex.getMessage()); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @StubRest(container = TestContainerPolicy.GRIZZLY) + RestClient rest; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DebugReportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DebugReportTest.java new file mode 100644 index 000000000..f4d123c9a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DebugReportTest.java @@ -0,0 +1,58 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class DebugReportTest extends AbstractPlatformTest { + + @Test + void testDebugReport() { + + String res = run(Test1.class); + Assertions.assertThat(res).contains("REST stub (@StubRest) started on DebugReportTest$Test1:\n" + + "\n" + + "\tJersey test container factory: org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory\n" + + "\tDropwizard exception mappers: DISABLED\n" + + "\n" + + "\t2 resources (disabled 1):\n" + + "\t\tErrorResource (r.v.d.g.t.r.support) \n" + + "\t\tResource1 (r.v.d.g.t.r.support) \n" + + "\n" + + "\t2 jersey extensions (disabled 1):\n" + + "\t\tRestExceptionMapper (r.v.d.g.t.r.support) \n" + + "\t\tRestFilter1 (r.v.d.g.t.r.support) \n" + + "\n" + + "\tUse .printJerseyConfig() report to see ALL registered jersey extensions (including dropwizard)"); + } + + @TestGuiceyApp(value = RestStubApp.class, debug = true) + @Disabled + public static class Test1 { + + @StubRest(disableDropwizardExceptionMappers = true, + disableResources = Resource2.class, + disableJerseyExtensions = RestFilter2.class) + RestClient rest; + + @Test + void test() { + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DropwizardExtensionsDisableTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DropwizardExtensionsDisableTest.java new file mode 100644 index 000000000..81e90ff41 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/DropwizardExtensionsDisableTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import jakarta.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ + +public class DropwizardExtensionsDisableTest extends AbstractPlatformTest { + + @Test + void testDwMappersDisable() { + + String out = run(Test1.class); + Assertions.assertTrue(out.contains(">>>ERROR:\nerror")); + + out = run(Test2.class); + Assertions.assertTrue(out.contains(">>>ERROR:\n{\"code\":500,\"message\":\"There was an error processing your request. It has been logged")); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest; + + @Test + void testMappersDisabled() { + final WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/error/1", String.class)); + + System.out.println(">>>ERROR:\n" + ex.getResponse().readEntity(String.class)); + } + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test2 { + + @StubRest + RestClient rest; + + @Test + void testMappersDisabled() { + final WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/error/1", String.class)); + + System.out.println(">>>ERROR:\n" + ex.getResponse().readEntity(String.class)); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ErrorsHandlingTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ErrorsHandlingTest.java new file mode 100644 index 000000000..49976d766 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ErrorsHandlingTest.java @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import jakarta.ws.rs.ProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 19.09.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class ErrorsHandlingTest { + + // disable all exception mappers to receive raw exceptions + @StubRest(disableDropwizardExceptionMappers = true, disableAllJerseyExtensions = true) + RestClient rest; + + @Test + void testErrorPropagation() { + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, () -> + rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } + + @Test + void testErrors() { + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, () -> rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + ex = Assertions.assertThrows(ProcessingException.class, () -> rest.post("/error/foo", null, String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + ex = Assertions.assertThrows(ProcessingException.class, () -> rest.put("/error/foo", "something", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + ex = Assertions.assertThrows(ProcessingException.class, () -> rest.patch("/error/foo", "something", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + ex = Assertions.assertThrows(ProcessingException.class, () -> rest.delete("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ExactExtensionsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ExactExtensionsTest.java new file mode 100644 index 000000000..8c8d47171 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/ExactExtensionsTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import jakarta.ws.rs.ProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class ExactExtensionsTest { + + @StubRest(value = {Resource1.class, ErrorResource.class}, + jerseyExtensions = RestFilter1.class, + disableDropwizardExceptionMappers = true) + RestClient rest; + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testExtensionsDisabled() { + Assertions.assertNotNull(rest); + + // extensions enabled + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + ErrorResource.class, + RestFilter1.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource2.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + } + + @Test + void testExceptionMapperNotSet() { + + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/Hk2RestSupportTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/Hk2RestSupportTest.java new file mode 100644 index 000000000..b8e0242b6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/Hk2RestSupportTest.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyManaged; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class Hk2RestSupportTest { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.extensions(HkResource.class); + + @StubRest(HkResource.class) + RestClient rest; + + @Test + void testHkRest() { + + String res = rest.get("/hk/", String.class); + Assertions.assertEquals(res, "foo"); + } + + @Path("/hk/") + @JerseyManaged + public static class HkResource { + + @GET + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreAllJerseyExtTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreAllJerseyExtTest.java new file mode 100644 index 000000000..0c152e2bb --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreAllJerseyExtTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import jakarta.ws.rs.ProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class IgnoreAllJerseyExtTest { + + @StubRest(disableAllJerseyExtensions = true, disableDropwizardExceptionMappers = true) + RestClient rest; + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testExtensionsDisabled() { + Assertions.assertNotNull(rest); + + // extensions enabled + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + Resource2.class, + ErrorResource.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + RestFilter1.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + } + + @Test + void testExceptionMapperNotSet() { + + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreExtensionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreExtensionTest.java new file mode 100644 index 000000000..1324c0fdf --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IgnoreExtensionTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import jakarta.ws.rs.ProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class IgnoreExtensionTest { + + @StubRest(disableResources = Resource2.class, + disableJerseyExtensions = {RestFilter2.class, RestExceptionMapper.class}, + disableDropwizardExceptionMappers = true) + RestClient rest; + + @Inject + GuiceyConfigurationInfo info; + + @Test + void testExtensionsDisabled() { + Assertions.assertNotNull(rest); + + // extensions enabled + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + ErrorResource.class, + RestFilter1.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource2.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + } + + @Test + void testExceptionMapperNotSet() { + + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IncorrectDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IncorrectDeclarationTest.java new file mode 100644 index 000000000..47acf6dd6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/IncorrectDeclarationTest.java @@ -0,0 +1,69 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +public class IncorrectDeclarationTest extends AbstractPlatformTest { + + @Test + void testUsageWithDwTest() { + + final Throwable ex = runFailed(Test1.class); + Assertions.assertEquals("Resources stubbing is useless when application is fully started. Use it with " + + "@TestGuiceyApp where web services not started in order to start lightweight container with rest services.", ex.getMessage()); + } + + @Test + void testMultipleFields() { + + final Throwable ex = runFailed(Test2.class); + Assertions.assertEquals("Multiple @StubRest fields declared. To avoid confusion with the configuration, " + + "only one field is supported.", ex.getMessage()); + } + + @TestDropwizardApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @StubRest + RestClient rest; + + @Test + void test() { + + } + } + + @TestDropwizardApp(RestStubApp.class) + @Disabled + public static class Test2 { + + @StubRest + RestClient rest; + + + @StubRest + RestClient rest2; + + @Test + void test() { + + } + } + + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/JerseyReportCompatibilityTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/JerseyReportCompatibilityTest.java new file mode 100644 index 000000000..ad45b22b0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/JerseyReportCompatibilityTest.java @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ +public class JerseyReportCompatibilityTest extends AbstractPlatformTest { + + @Test + void testJerseyReport() { + String out = run(Test1.class); + + Assertions.assertThat(out).contains("ru.vyarus.dropwizard.guice.debug.JerseyConfigDiagnostic: Jersey configuration = \n" + + "\n" + + " Exception mappers\n" + + " Throwable ExceptionMapperBinder$1 (i.d.core.setup) \n" + + " Throwable DefaultExceptionMapper (o.g.jersey.server) \n" + + " EofException EarlyEofExceptionMapper (i.d.jersey.errors) \n" + + " EmptyOptionalException EmptyOptionalExceptionMapper (i.d.jersey.optional) \n" + + " IllegalStateException IllegalStateExceptionMapper (i.d.jersey.errors) \n" + + " JerseyViolationException JerseyViolationExceptionMapper (i.d.j.validation) \n" + + " JsonProcessingException JsonProcessingExceptionMapper (i.d.jersey.jackson) \n" + + " Exception RestExceptionMapper (r.v.d.g.t.r.support) \n" + + " ValidationException ValidationExceptionMapper (o.g.j.s.v.internal)"); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @EnableHook + static GuiceyConfigurationHook hook = GuiceBundle.Builder::printJerseyConfig; + + @StubRest + RestClient rest; + + @Test + void test() { + + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MethodMatchTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MethodMatchTest.java new file mode 100644 index 000000000..27c156838 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MethodMatchTest.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.client.ResourceClient; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 18.09.2025 + */ +@TestGuiceyApp(value = RestStubApp.class, apacheClient = true) +public class MethodMatchTest { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest; + + @Test + void testMethodSearch() { + final ResourceClient res1 = rest.restClient(Resource1.class); + + String res = res1.method(mock -> mock.get("test", null)) +// .pathParam("foo", "test") + .as(String.class); + Assertions.assertEquals("test", res); + + res = res1.method("get") + .pathParam("foo", "test") + .as(String.class); + Assertions.assertEquals("test", res); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MocksCompatibilityTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MocksCompatibilityTest.java new file mode 100644 index 000000000..86fc6a443 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/MocksCompatibilityTest.java @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class MocksCompatibilityTest { + + @EnableHook + static GuiceyConfigurationHook hook = builder -> builder.extensions(TestResource.class); + + @StubRest(TestResource.class) + RestClient rest; + + @MockBean + Service service; + + @Test + void testMocksSupport() { + Mockito.when(service.foo()).thenReturn("bar"); + + String res = rest.get("/test/", String.class); + Assertions.assertEquals("bar", res); + } + + @Path("/test/") + public static class TestResource { + @Inject + Service service; + + @GET + @Path("/") + public String get() { + return service.foo(); + } + } + + public static class Service { + + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/NestedTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/NestedTest.java new file mode 100644 index 000000000..b33efc941 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/NestedTest.java @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class NestedTest { + + @StubRest + RestClient rest; + + @Test + void test() { + Assertions.assertNotNull(rest); + } + + @Nested + public class Nest { + @Test + void test() { + Assertions.assertNotNull(rest); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientDebugTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientDebugTest.java new file mode 100644 index 000000000..f7524db2b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientDebugTest.java @@ -0,0 +1,78 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +public class RestClientDebugTest extends AbstractPlatformTest { + + @Test + void testClientDebug() { + String out = run(Test1.class); + + Assertions.assertThat(out).contains("[Client action]---------------------------------------------{\n" + + "1 * Sending client request on thread ddd\n" + + "1 > GET http://localhost:0/1/foo\n" + + "\n" + + "}----------------------------------------------------------\n" + + "\n" + + "\n" + + "[Client action]---------------------------------------------{\n" + + "1 * Client response received on thread ddd\n" + + "1 < 200\n" + + "1 < Content-Length: 3\n" + + "1 < Content-Type: application/json\n" + + "foo\n" + + "\n" + + "}----------------------------------------------------------"); + } + + @Test + void testClientDebugDisabled() { + String out = run(Test2.class); + + Assertions.assertThat(out).doesNotContain("[Client action]---------------------------------------------{"); + } + + @Override + protected String clean(String out) { + return out.replaceAll("on thread ([^\n]+)", "on thread ddd"); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test1 { + + @StubRest + RestClient rest; + + @Test + void test() { + String res = rest.get("/1/foo", String.class); + org.junit.jupiter.api.Assertions.assertEquals("foo", res); + } + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class Test2 { + + @StubRest(logRequests = false) + RestClient rest; + + @Test + void test() { + String res = rest.get("/1/foo", String.class); + org.junit.jupiter.api.Assertions.assertEquals("foo", res); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientStateResetTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientStateResetTest.java new file mode 100644 index 000000000..fc1d63734 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientStateResetTest.java @@ -0,0 +1,80 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 26.02.2025 + */ +public class RestClientStateResetTest extends AbstractPlatformTest { + + @Test + void testAutoRest() { + runSuccess(Test1.class, Test2.class); + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + public static class Test1 { + + @StubRest + RestClient rest; + + @Test + @Order(1) + void test() { + rest.defaultHeader("Foo", "bar"); + rest.defaultAccept("dsfdsfd"); + rest.defaultQueryParam("foo", "bar"); + } + + @Test + @Order(2) + void test2() { + Assertions.assertFalse(rest.hasDefaultAccepts()); + Assertions.assertFalse(rest.hasDefaultHeaders()); + Assertions.assertFalse(rest.hasDefaultQueryParams()); + } + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + public static class Test2 { + + @StubRest(autoReset = false) + RestClient rest; + + @Test + @Order(1) + void test() { + rest.defaultHeader("Foo", "bar"); + rest.defaultAccept("dsfdsfd"); + rest.defaultQueryParam("foo", "bar"); + } + + @Test + @Order(2) + void test2() { + Assertions.assertTrue(rest.hasDefaultAccepts()); + Assertions.assertTrue(rest.hasDefaultHeaders()); + Assertions.assertTrue(rest.hasDefaultQueryParams()); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientTest.java new file mode 100644 index 000000000..ec22a5180 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestClientTest.java @@ -0,0 +1,95 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Entity; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 25.02.2025 + */ +@TestGuiceyApp(RestStubApp.class) +public class RestClientTest { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest; + + @Test + void testClient() { + Assertions.assertNotNull(rest.client()); + Assertions.assertNotNull(rest.getBaseUri()); + Assertions.assertEquals(0, rest.getBaseUri().getPort()); + + Assertions.assertFalse(rest.hasDefaultAccepts()); + Assertions.assertFalse(rest.hasDefaultHeaders()); + Assertions.assertFalse(rest.hasDefaultQueryParams()); + + String res = rest.target("/1/foo").request().get(String.class); + Assertions.assertEquals("foo", res); + + res = rest.get("/1/bar", String.class); + Assertions.assertEquals("bar", res); + + rest.post("/1/foo", null); + res = rest.put("/1/foo", "something", String.class); + Assertions.assertEquals("foo", res); + + // patch body can't be null + rest.patch("/1/foo", Entity.text("sample")); + res = rest.patch("/1/foo", "something", String.class); + Assertions.assertEquals("foo", res); + + rest.delete("/1/bar"); + } + + @Test + void testErrors() { + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, () -> rest.get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + + ex = Assertions.assertThrows(WebApplicationException.class, () -> rest.post("/error/foo", null, String.class)); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + + ex = Assertions.assertThrows(WebApplicationException.class, () -> rest.put("/error/foo", "something", String.class)); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + + ex = Assertions.assertThrows(WebApplicationException.class, () -> rest.patch("/error/foo", "something", String.class)); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + + ex = Assertions.assertThrows(WebApplicationException.class, () -> rest.delete("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + } + + @Test + void testSuccessVoidCalls() { + Assertions.assertNull(rest.get("/1/foo", Void.class)); + Assertions.assertNull(rest.post("/1/foo", null, Void.class)); + Assertions.assertNull(rest.put("/1/foo", "something", Void.class)); + Assertions.assertNull(rest.delete("/1/foo", Void.class)); + } + + @Test + void testVoidStatusCheck() { + AssertionFailedError ex = Assertions.assertThrows(AssertionFailedError.class, () -> + rest.buildGet("/1/foo").expectSuccess(205)); + Assertions.assertTrue(ex.getMessage().contains("Unexpected response status 200")); + + ex = Assertions.assertThrows(AssertionFailedError.class, () -> + rest.buildPost("/1/foo", null).expectSuccess(205)); + Assertions.assertTrue(ex.getMessage().contains("Unexpected response status 204")); + + ex = Assertions.assertThrows(AssertionFailedError.class, () -> + rest.buildPut("/1/foo", "something").expectSuccess(205)); + Assertions.assertTrue(ex.getMessage().contains("Unexpected response status 200")); + + ex = Assertions.assertThrows(AssertionFailedError.class, () -> + rest.buildDelete("/1/foo").expectSuccess(205)); + Assertions.assertTrue(ex.getMessage().contains("Unexpected response status 204")); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestStubSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestStubSpockTest.groovy new file mode 100644 index 000000000..c7496bce6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/RestStubSpockTest.groovy @@ -0,0 +1,85 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest + +import com.google.inject.Inject +import jakarta.ws.rs.WebApplicationException +import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.rest.RestClient +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1 +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2 +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1 +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2 +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = RestStubApp, debug = true) +class RestStubSpockTest extends AbstractTest { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest + + @Inject + GuiceyConfigurationInfo info + + @Inject + RestFilter1 filter + + @Inject + ManagedBean managed + + @Inject + RestExceptionMapper exceptionMapper + + def "Check rest stub"() { + + expect: + rest + + // extensions enabled + [Resource1, + Resource2, + ErrorResource, + RestFilter1, + RestFilter2, + ManagedBean, + RestExceptionMapper, + HK2DebugFeature] as Set == info.getExtensions() as Set + + // web extension auto disabled + info.getExtensionsDisabled().contains(WebFilter) + + // managed called once + 1 == managed.beforeCnt + 0 == managed.afterCnt + + when: + String res = rest.get("/1/foo", String) + + then: + "foo" == res + + // rest filter used + filter.called + } + + def "Check rest error"() { + + when: + rest.get("/error/foo", String) + + then: + def ex = thrown(WebApplicationException) + exceptionMapper.called + "error" == ex.getResponse().readEntity(String) + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SameDwExtensionsInStubTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SameDwExtensionsInStubTest.java new file mode 100644 index 000000000..bd8bbc1b9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SameDwExtensionsInStubTest.java @@ -0,0 +1,81 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; +import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfig; +import ru.vyarus.dropwizard.guice.debug.report.jersey.JerseyConfigRenderer; +import ru.vyarus.dropwizard.guice.module.context.option.Options; +import ru.vyarus.dropwizard.guice.module.installer.InstallersOptions; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * Important because dropwizard extensions registered manually (same as in dropwizard test: + * io.dropwizard.testing.common.DropwizardTestResourceConfig)! But they must be in sync with server implementation. + * + * @author Vyacheslav Rusakov + * @since 24.02.2025 + */ +public class SameDwExtensionsInStubTest extends AbstractPlatformTest { + + static String dwReport; + static String stubReport; + + @Test + void testSameExtensions() { + + run(TestDw.class); + run(TestStub.class); + + Assertions.assertEquals(dwReport, stubReport); + } + + @TestDropwizardApp(RestStubApp.class) + @Disabled + public static class TestDw { + + @Inject + Options options; + @Inject + InjectionManager injectionManager; + + @Test + void test() { + final Boolean guiceFirstMode = options.get(InstallersOptions.JerseyExtensionsManagedByGuice); + dwReport = new JerseyConfigRenderer(injectionManager, guiceFirstMode) + .renderReport(new JerseyConfig()); + } + } + + @TestGuiceyApp(RestStubApp.class) + @Disabled + public static class TestStub { + + @StubRest + RestClient rest; + + @Inject + Options options; + @Inject + InjectionManager injectionManager; + + @Test + void test() { + final Boolean guiceFirstMode = options.get(InstallersOptions.JerseyExtensionsManagedByGuice); + stubReport = new JerseyConfigRenderer(injectionManager, guiceFirstMode) + .renderReport(new JerseyConfig()); + } + } + + @Override + protected String clean(String out) { + return out; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SimpleResourceTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SimpleResourceTest.java new file mode 100644 index 000000000..846c5da3b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/rest/SimpleResourceTest.java @@ -0,0 +1,84 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.rest; + +import com.google.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.rest.RestClient; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.StubRest; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 20.02.2025 + */ +@TestGuiceyApp(value = RestStubApp.class, debug = true) +public class SimpleResourceTest { + + @StubRest(disableDropwizardExceptionMappers = true) + RestClient rest; + + @Inject + GuiceyConfigurationInfo info; + + @Inject + RestFilter1 filter; + + @Inject + ManagedBean managed; + + @Inject + RestExceptionMapper exceptionMapper; + + @Test + void testRestStub() { + Assertions.assertNotNull(rest); + + // extensions enabled + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + Resource2.class, + ErrorResource.class, + RestFilter1.class, + RestFilter2.class, + ManagedBean.class, + RestExceptionMapper.class)), + new HashSet(info.getExtensions())); + + // web extension auto disabled + Assertions.assertTrue(info.getExtensionsDisabled().contains(WebFilter.class)); + + // managed called once + Assertions.assertEquals(1, managed.beforeCnt); + Assertions.assertEquals(0, managed.afterCnt); + + String res = rest.get("/1/foo", String.class); + Assertions.assertEquals("foo", res); + + // rest filter used + Assertions.assertTrue(filter.called); + } + + @Test + void testError() { + + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.get("/error/foo", String.class)); + Assertions.assertTrue(exceptionMapper.called); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/AbstractSpyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/AbstractSpyTest.java new file mode 100644 index 000000000..77cf9aa74 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/AbstractSpyTest.java @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +public abstract class AbstractSpyTest extends AbstractPlatformTest { + + @Override + protected String clean(String out) { + return out + .replaceAll("@[\\da-z]{6,10}", "@11111111") + .replaceAll("\\$\\$[\\da-z]{6,10}", "\\$\\$11111111") + .replaceAll("hashCode: \\d+", "hashCode: 11111111") + .replaceAll("\\d+(\\.\\d+)? ms", "11.11 ms"); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/IncorrectSpyDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/IncorrectSpyDeclarationTest.java new file mode 100644 index 000000000..ec81fffb0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/IncorrectSpyDeclarationTest.java @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MockBean; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; +import ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.IncorrectMockDeclarationTest; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +public class IncorrectSpyDeclarationTest extends AbstractSpyTest { + + @Test + void testIncorrectManualSpyDeclaration() { + + Throwable th = runFailed(Test1.class); + + Assertions.assertThat(th.getMessage()).contains( + "Incorrect @SpyBean 'IncorrectSpyDeclarationTest$Test1.serviceMock' declaration: manual spy " + + "declaration is not supported. Use @MockBean instead to specify manual spy object."); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + static Service mock = new Service(); + + @SpyBean + static Service serviceMock = mock; + + @Test + void testManualMock() { + Assertions.assertThat(serviceMock).isSameAs(mock); + } + + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/InstanceSpyDetectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/InstanceSpyDetectionTest.java new file mode 100644 index 000000000..c357cf96b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/InstanceSpyDetectionTest.java @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +public class InstanceSpyDetectionTest extends AbstractSpyTest { + + @Test + void testInstanceSpyDetection() { + + Throwable th = runFailed(Test1.class); + + Assertions.assertThat(th.getMessage()).isEqualTo( + "Incorrect @SpyBean 'InstanceSpyDetectionTest$Test1.spy' declaration: target bean 'Service' " + + "bound by instance and so can't be spied"); + } + + @TestGuiceyApp(Test1.App.class) + @Disabled + public static class Test1 { + @Inject + Test1.Service service; + + @SpyBean + Test1.Service spy; + + @Test + void testInstanceTracking() { + // nothing + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .modules(builder -> builder.bind(Test1.Service.class).toInstance(new Test1.Service())) + .build(); + } + } + + public static class Service { + + public String foo(int i) { + return "foo" + i; + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyCleanupDisableTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyCleanupDisableTest.java new file mode 100644 index 000000000..9363cbcd1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyCleanupDisableTest.java @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class SpyCleanupDisableTest { + + @SpyBean(autoReset = false) + static Service service; + + @Test + void testMethod() { + Mockito.when(service.foo()).thenReturn("bar"); + } + + @AfterAll + static void afterAll() { + // spy auto cleaned after each test - without cleanup, here will be overridden method + Assertions.assertThat(service.foo()).isEqualTo("bar"); + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyInitializerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyInitializerTest.java new file mode 100644 index 000000000..a433861b4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyInitializerTest.java @@ -0,0 +1,61 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +import java.util.function.Consumer; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +@TestGuiceyApp(SpyInitializerTest.App.class) +public class SpyInitializerTest { + + @SpyBean(initializers = Initializer.class) + Service1 spy1; + + @Test + void testInitializer() { + Mockito.verify(spy1, Mockito.times(1)).get(11); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Mng.class) + .build(); + } + } + + public static class Service1 { + + public String get(int id) { + return "Hello " + id; + } + } + + public static class Mng implements Managed { + @Inject + Service1 service1; + + @Override + public void start() throws Exception { + service1.get(11); + } + } + + public static class Initializer implements Consumer { + @Override + public void accept(Service1 service1) { + Mockito.doReturn("spied").when(service1).get(11); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyMethodResultTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyMethodResultTest.java new file mode 100644 index 000000000..c65221d16 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyMethodResultTest.java @@ -0,0 +1,56 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 18.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class SpyMethodResultTest { + + @SpyBean + Service spy; + + @Test + void testResultCapture() { + ResultCaptor resultCaptor = new ResultCaptor<>(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + Mockito.doAnswer(resultCaptor).when(spy).foo(argumentCaptor.capture()); + + // call method + Assertions.assertThat(spy.foo(11)).isEqualTo("foo11"); + // result captured + Assertions.assertThat(resultCaptor.getResult()).isEqualTo("foo11"); + Assertions.assertThat(argumentCaptor.getValue()).isEqualTo(11); + + Mockito.verify(spy, Mockito.times(1)).foo(11); + } + + public static class ResultCaptor implements Answer { + private T result = null; + public T getResult() { + return result; + } + + @Override + public T answer(InvocationOnMock invocationOnMock) throws Throwable { + result = (T) invocationOnMock.callRealMethod(); + return result; + } + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyResetTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyResetTest.java new file mode 100644 index 000000000..f988b37fc --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpyResetTest.java @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class SpyResetTest { + + @SpyBean + Service spy; + + @Test + void test1() { + spy.foo(); + Mockito.verify(spy).foo(); + } + + @Test + void test2() { + spy.foo(); + // no second exec (put autoReset = false to make sure) + Mockito.verify(spy).foo(); + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySimpleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySimpleTest.java new file mode 100644 index 000000000..bc566321a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySimpleTest.java @@ -0,0 +1,77 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.mockito.internal.util.MockUtil; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 10.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +public class SpySimpleTest { + + @SpyBean + Service1 spy1; + @SpyBean + static Service2 spy2; + + + @Inject + OuterService outerService; + + @BeforeAll + static void beforeAll() { + Assertions.assertNotNull(spy2); + } + + @BeforeEach + void setUp() { + Assertions.assertNotNull(spy1); + Assertions.assertNotNull(spy2); + } + + @Test + void testSpyInjection() { + Assertions.assertNotNull(spy1); + Assertions.assertNotNull(spy2); + Assertions.assertTrue(MockUtil.isSpy(spy1)); + Assertions.assertTrue(MockUtil.isSpy(spy2)); + Assertions.assertEquals("Hello 11 Hello 11", outerService.doSomething(11)); + Mockito.verify(spy1, Mockito.times(1)).get(11); + Mockito.verify(spy2, Mockito.times(1)).get(11); + } + + public static class Service1 { + + public String get(int id) { + return "Hello " + id; + } + } + + public static class Service2 { + + public String get(int id) { + return "Hello " + id; + } + } + + public static class OuterService { + + @Inject + Service1 service1; + @Inject + Service2 service2; + + public String doSomething(int id) { + return service1.get(id) + " " + service2.get(id); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySpockTest.groovy new file mode 100644 index 000000000..a716316f6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySpockTest.groovy @@ -0,0 +1,54 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy + + +import jakarta.inject.Inject +import org.mockito.Mockito +import org.mockito.internal.util.MockUtil +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = DefaultTestApp, debug = true) +class SpySpockTest extends Specification { + + // could be used only with JAVA classes + @SpyBean + SpySimpleTest.Service1 spy1 + @SpyBean + static SpySimpleTest.Service2 spy2 + + + @Inject + SpySimpleTest.OuterService outerService + + void setupSpec() { + assert spy2 + } + + void setup() { + assert spy1 + assert spy2 + } + + def "Check spy"() { + + expect: + spy1 + spy2 + MockUtil.isSpy(spy1) + MockUtil.isSpy(spy2) + "Hello 11 Hello 11" == outerService.doSomething(11) + + when: + Mockito.verify(spy1, Mockito.times(1)).get(11) + Mockito.verify(spy2, Mockito.times(1)).get(11) + + then: + true + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySummaryTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySummaryTest.java new file mode 100644 index 000000000..879dfc5df --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/spy/SpySummaryTest.java @@ -0,0 +1,72 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.spy; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; + +/** + * @author Vyacheslav Rusakov + * @since 18.02.2025 + */ +public class SpySummaryTest extends AbstractSpyTest { + + @Test + void testSpySummary() { + + String out = run(Test1.class); + + Assertions.assertThat(out).contains("@SpyBean stats on [After each] for SpySummaryTest$Test1#test():\n" + + "\n" + + "\t[Mockito] Interactions of: ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Service$$EnhancerByGuice$$11111111@11111111\n" + + "\t 1. spySummaryTest$Service$$EnhancerByGuice$$11111111.foo(\n" + + "\t 1\n" + + "\t);\n" + + "\t -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Test1.test(SpySummaryTest.java:50)"); + } + + + @Test + void testNoSpySummary() { + + String out = run(Test2.class); + + Assertions.assertThat(out).contains("@SpyBean stats on [After each] for SpySummaryTest$Test2#test():\n" + + "\n" + + "\tNo interactions and stubbings found for mock: ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Service$$EnhancerByGuice$$11111111@11111111"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + @SpyBean(printSummary = true) + Service service; + + @Test + void test() { + Assertions.assertThat(service.foo(1)).isEqualTo("foo1"); + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + + @SpyBean(printSummary = true) + Service service; + + @Test + void test() { + // no mock actions + } + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/FieldValueChangeDetectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/FieldValueChangeDetectionTest.java new file mode 100644 index 000000000..ee7ee50e4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/FieldValueChangeDetectionTest.java @@ -0,0 +1,109 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +public class FieldValueChangeDetectionTest { + + String msg; + + @Test + void checkFieldOverrideDetection() { + + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + err.printStackTrace(); + msg = err.getMessage(); + }); + + org.junit.jupiter.api.Assertions.assertEquals("Field FieldValueChangeDetectionTest$Test1.stub1 " + + "annotated with @StubBean value was changed: most likely, it happen in test setup method, which is " + + "called after Injector startup and so too late to change binding values. Manual initialization is possible " + + "in field directly.", msg); + } + + @Test + void checkStaticFieldOverrideDetection() { + + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test2.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + msg = err.getMessage(); + }); + + org.junit.jupiter.api.Assertions.assertEquals("Field FieldValueChangeDetectionTest$Test2.stub2 " + + "annotated with @StubBean value was changed: most likely, it happen in test setup method, which is called " + + "after Injector startup and so too late to change binding values. Manual initialization is possible " + + "in field directly.", msg); + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + @StubBean(Service1.class) + Service1Stub stub1; + + @BeforeEach + void setUp() { + Assertions.assertThat(stub1).isNotNull(); + stub1 = new Service1Stub(); + } + + @Test + void testValueOverrideDetection() { + // error after test + } + + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + + @StubBean(Service2.class) + static Service2Stub stub2; + + @BeforeAll + static void beforeAll() { + Assertions.assertThat(stub2).isNotNull(); + stub2 = new Service2Stub(); + } + + @Test + void testValueOverrideDetection() { + // error after test + } + } + + public static class Service1Stub extends Service1 {} + + public static class Service2Stub extends Service2 {} + + public static class Service1 {} + + public static class Service2 {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectManualStubDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectManualStubDeclarationTest.java new file mode 100644 index 000000000..2ad5dc052 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectManualStubDeclarationTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; +import uk.org.webcompere.systemstubs.stream.SystemOut; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +@ExtendWith(SystemStubsExtension.class) +public class IncorrectManualStubDeclarationTest { + + @SystemStub + SystemOut out; + + String msg; + + @Test + void checkStubValidation() { + + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + msg = err.getMessage(); + }); + System.err.println(out.getText()); + + Assertions.assertEquals("Incorrect @StubBean 'IncorrectManualStubDeclarationTest$Test1.stub' " + + "declaration: field value can't be used because guice context starts in beforeAll phase. Either make " + + "field static or remove value (guice will create instance with guice injector)", msg); + } + + + @TestGuiceyApp(value = DefaultTestApp.class, debug = true) + @Disabled // prevent direct execution + public static class Test1 { + + @StubBean(Test1.Service.class) + Test1.ServiceStub stub = new Test1.ServiceStub(); + + @Test + void test() { + Assertions.assertNotNull(stub); + } + + public static class Service {} + + public static class ServiceStub extends Service{} + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectNestedStubDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectNestedStubDeclarationTest.java new file mode 100644 index 000000000..4fed0e219 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectNestedStubDeclarationTest.java @@ -0,0 +1,89 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; +import uk.org.webcompere.systemstubs.stream.SystemOut; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +@ExtendWith(SystemStubsExtension.class) +public class IncorrectNestedStubDeclarationTest { + + @SystemStub + SystemOut out; + + String msg; + + @Test + void checkStubValidation() { + + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + msg = err.getMessage(); + }); + System.err.println(out.getText()); + + Assertions.assertEquals("Incorrect @StubBean 'IncorrectNestedStubDeclarationTest$Test1$Inner.stub2' " + + "declaration: nested test runs under already started application and so new fields could not be added. " + + "Either remove annotated fields in nested tests or run application for each test method (with non-static @RegisterExtension field)", msg); + } + + @TestGuiceyApp(value = DefaultTestApp.class, debug = true) + @Disabled // prevent direct execution + public static class Test1 { + + @StubBean(Service.class) + ServiceStub stub; + + @Test + void testStub() { + Assertions.assertNotNull(stub); + } + + @Nested + class Inner { + + @Inject + Service service; + + @Inject + Service2 service2; + + @StubBean(Service2.class) + Service2Stub stub2; + + @Test + void testStubUsed() { + Assertions.assertNotNull(stub2); + Assertions.assertEquals(service, stub); + Assertions.assertEquals(service2, stub2); + } + } + + public static class Service {} + public static class ServiceStub extends Service {} + + public static class Service2 {} + public static class Service2Stub extends Service2 {} + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectStubDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectStubDeclarationTest.java new file mode 100644 index 000000000..206ee7ef7 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/IncorrectStubDeclarationTest.java @@ -0,0 +1,56 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +public class IncorrectStubDeclarationTest { + + String msg; + + @Test + void checkStubValidation() { + + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors(selectClass(Test1.class)) + .execute().allEvents().failed().stream() + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + msg = err.getMessage(); + }); + + Assertions.assertEquals("Incorrect @StubBean 'IncorrectStubDeclarationTest$Test1.stub' " + + "declaration: ServiceStub is not assignable to Service", msg); + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled // prevent direct execution + public static class Test1 { + + @StubBean(Service.class) + ServiceStub stub; + + @Test + void test() { + Assertions.assertNotNull(stub); + } + + public static class Service {} + + public static class ServiceStub {} + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/InterfaceStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/InterfaceStubsTest.java new file mode 100644 index 000000000..4a4720615 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/InterfaceStubsTest.java @@ -0,0 +1,142 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +@TestGuiceyApp(value = InterfaceStubsTest.App.class, debug = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class InterfaceStubsTest { + + @Inject + IService1 service1; + + @Inject + IService2 service2; + + @Inject + Service service; + + @StubBean(IService1.class) + Service1Stub stub; + + @StubBean(IService2.class) + static Service2Stub stub2; + + @BeforeAll + static void beforeAll() { + Preconditions.checkNotNull(stub2); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Preconditions.checkNotNull(stub2); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .modules(binder -> { + binder.bind(IService1.class).to(Service1Impl.class); + binder.bind(IService2.class).to(Service2Impl.class); + }) + .build(); + } + } + + public static class Service1Stub implements IService1, StubLifecycle { + + static boolean created; + + public boolean beforeCalled; + public boolean afterCalled; + + public Service1Stub() { + Preconditions.checkState(!created); + created = true; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub implements IService2 {} + + public interface IService1 { + String get(); + } + public static class Service1Impl implements IService1 { + + @Override + public String get() { + return "sun"; + } + } + + + public interface IService2 {} + public static class Service2Impl implements IService2 { + } + + @Singleton + public static class Service { + + @Inject + IService1 service1; + + public String get() { + return service1.get(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/ManualStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/ManualStubsTest.java new file mode 100644 index 000000000..a6ea2e97e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/ManualStubsTest.java @@ -0,0 +1,139 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ManualStubsTest { + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @Inject + Service service; + + @StubBean(Service1.class) + Service1Stub stub = new Service1Stub("manual"); + + @StubBean(Service2.class) + static Service2Stub stub2 = new Service2Stub("manual"); + + @BeforeAll + static void beforeAll() { + Preconditions.checkNotNull(stub2); + Assertions.assertEquals("manual", stub2.source); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Assertions.assertEquals("manual", stub.source); + Preconditions.checkNotNull(stub2); + Assertions.assertEquals("manual", stub2.source); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("manual", stub.source); + Assertions.assertEquals("manual", stub2.source); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("manual", stub.source); + Assertions.assertEquals("manual", stub2.source); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + static boolean created; + + public boolean beforeCalled; + public boolean afterCalled; + + public String source; + + public Service1Stub(String source) { + Preconditions.checkState(!created); + created = true; + this.source = source; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub extends Service2 { + public String source; + + public Service2Stub(String source) { + this.source = source; + } + } + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service2 { + } + + @Singleton + public static class Service { + + @Inject + Service1 service1; + + public String get() { + return service1.get(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerClassStubTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerClassStubTest.java new file mode 100644 index 000000000..7819a334d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerClassStubTest.java @@ -0,0 +1,41 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class NestedPerClassStubTest { + + @StubBean(Service.class) + ServiceStub stub; + + @Test + void testStub() { + Assertions.assertNotNull(stub); + } + + @Nested + class Inner { + + @Inject + Service service; + + @Test + void testStubUsed() { + Assertions.assertEquals(service, stub); + } + } + + public static class Service {} + + public static class ServiceStub extends Service {} +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerMethodStubTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerMethodStubTest.java new file mode 100644 index 000000000..7e14b1658 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/NestedPerMethodStubTest.java @@ -0,0 +1,127 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +public class NestedPerMethodStubTest { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp.class) + .debug().create(); + + @StubBean(Service.class) + ServiceStub stub; + + @StubBean(Service2.class) + static Service2Stub stub2 = new Service2Stub("manual"); + + @Test + void testStub() { + Assertions.assertNotNull(stub); + Assertions.assertNotNull(stub2); + } + + @Nested + class Inner { + + @Inject + Service service; + + @Inject + Service2 service2; + + @Inject + Service3 service3; + + @Inject + Service4 service4; + + @StubBean(Service3.class) + Service3Stub stub3; + + @StubBean(Service4.class) + static Service4Stub stub4 = new Service4Stub("manual"); + + @Test + void testStubUsed() { + Assertions.assertNotNull(stub); + Assertions.assertNotNull(stub2); + Assertions.assertNotNull(stub3); + Assertions.assertNotNull(stub4); + Assertions.assertEquals(service, stub); + Assertions.assertEquals("bar", service.foo()); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("bar", service2.foo()); + Assertions.assertEquals(service3, stub3); + Assertions.assertEquals("bar", service3.foo()); + Assertions.assertEquals(service4, stub4); + Assertions.assertEquals("bar", service4.foo()); + } + } + + public static class Service { + public String foo() { + return "foo"; + } + } + public static class ServiceStub extends Service { + @Override + public String foo() { + return "bar"; + } + } + + public static class Service2 { + public String foo() { + return "foo"; + } + } + public static class Service2Stub extends Service2 { + + public Service2Stub(String custom) { + } + + @Override + public String foo() { + return "bar"; + } + } + + public static class Service3 { + public String foo() { + return "foo"; + } + } + public static class Service3Stub extends Service3 { + @Override + public String foo() { + return "bar"; + } + } + + public static class Service4 { + public String foo() { + return "foo"; + } + } + public static class Service4Stub extends Service4 { + + public Service4Stub(String custom) { + } + + @Override + public String foo() { + return "bar"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerClassStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerClassStubsTest.java new file mode 100644 index 000000000..c5f87f101 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerClassStubsTest.java @@ -0,0 +1,124 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerClassStubsTest { + + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @Inject + Service service; + + @StubBean(Service1.class) + Service1Stub stub; + + @StubBean(Service2.class) + static Service2Stub stub2; + + @BeforeAll + static void beforeAll() { + Preconditions.checkNotNull(stub2); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Preconditions.checkNotNull(stub2); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + static boolean created; + + public boolean beforeCalled; + public boolean afterCalled; + + public Service1Stub() { + Preconditions.checkState(!created); + created = true; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub extends Service2 {} + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service2 { + } + + @Singleton + public static class Service { + + @Inject + Service1 service1; + + public String get() { + return service1.get(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodManualStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodManualStubsTest.java new file mode 100644 index 000000000..fedcf0bac --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodManualStubsTest.java @@ -0,0 +1,130 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 09.02.2025 + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerMethodManualStubsTest { + + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp.class) + .debug() + .create(); + + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @Inject + Service service; + + @StubBean(Service1.class) + Service1Stub stub = new Service1Stub(); + + @StubBean(Service2.class) + static Service2Stub stub2 = new Service2Stub(); + + @BeforeAll + static void beforeAll() { + Preconditions.checkState(stub2 != null); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Preconditions.checkNotNull(stub2); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertEquals(1, stub.created); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertEquals(2, stub.created); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + public static int created; + + public boolean beforeCalled; + public static boolean afterCalled; + + public Service1Stub() { + created ++; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub extends Service2 {} + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service2 { + } + + @Singleton + public static class Service { + + @Inject + Service1 service1; + + public String get() { + return service1.get(); + } + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodStubsTest.java new file mode 100644 index 000000000..9043d622b --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/PerMethodStubsTest.java @@ -0,0 +1,129 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PerMethodStubsTest { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(DefaultTestApp.class) + .debug() + .create(); + + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @Inject + Service service; + + @StubBean(Service1.class) + Service1Stub stub; + + @StubBean(Service2.class) + static Service2Stub stub2; + + @BeforeAll + static void beforeAll() { + Preconditions.checkState(stub2 == null); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Preconditions.checkNotNull(stub2); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertEquals(1, stub.created); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertEquals(2, stub.created); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + public static int created; + + public boolean beforeCalled; + public static boolean afterCalled; + + public Service1Stub() { + created ++; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub extends Service2 {} + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service2 { + } + + @Singleton + public static class Service { + + @Inject + Service1 service1; + + public String get() { + return service1.get(); + } + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSimpleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSimpleTest.java new file mode 100644 index 000000000..2a0bb34fe --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSimpleTest.java @@ -0,0 +1,122 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import com.google.common.base.Preconditions; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle; + +/** + * @author Vyacheslav Rusakov + * @since 07.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class StubsSimpleTest { + + @Inject + Service1 service1; + + @Inject + Service2 service2; + + @Inject + Service service; + + @StubBean(Service1.class) + Service1Stub stub; + + @StubBean(Service2.class) + static Service2Stub stub2; + + @BeforeAll + static void beforeAll() { + Preconditions.checkNotNull(stub2); + } + + @BeforeEach + void setUp() { + Preconditions.checkNotNull(stub); + Preconditions.checkNotNull(stub2); + } + + @Test + @Order(1) + void testStub() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertFalse(stub.afterCalled); + } + + @Test + @Order(2) + void testStub2() { + Assertions.assertEquals(service1, stub); + Assertions.assertEquals(service2, stub2); + Assertions.assertEquals("moon", service.get()); + Assertions.assertTrue(stub.beforeCalled); + Assertions.assertTrue(stub.afterCalled); + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + static boolean created; + + public boolean beforeCalled; + public boolean afterCalled; + + public Service1Stub() { + Preconditions.checkState(!created); + created = true; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } + + public static class Service2Stub extends Service2 {} + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service2 { + } + + @Singleton + public static class Service { + + @Inject + Service1 service1; + + public String get() { + return service1.get(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSpockTest.groovy new file mode 100644 index 000000000..e72dc60ae --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/StubsSpockTest.groovy @@ -0,0 +1,103 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub + +import jakarta.inject.Inject +import jakarta.inject.Singleton +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean +import ru.vyarus.dropwizard.guice.test.stub.StubLifecycle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = DefaultTestApp, debug = true) +class StubsSpockTest extends Specification { + + + @Inject + Service1 service1 + + @Inject + Service2 service2 + + @Inject + Service service + + @StubBean(Service1) + Service1Stub stub + + @StubBean(Service2) + static Service2Stub stub2 + + void setupSpec() { + assert stub2 + } + + void setup() { + assert stub + assert stub2 + } + + def "Check stubs"() { + + expect: + service1 == stub + service2 == stub2 + "moon" == service.get() + stub.beforeCalled + !stub.afterCalled + } + + static class Service1Stub extends Service1 implements StubLifecycle { + + static boolean created + + public boolean beforeCalled + public boolean afterCalled + + Service1Stub() { + assert !created + created = true + } + + @Override + String get() { + return "moon" + } + + @Override + void before() { + beforeCalled = true + } + + @Override + void after() { + afterCalled = true + } + } + + static class Service2Stub extends Service2 {} + + static class Service1 { + + String get() { + return "sun" + } + } + + static class Service2 { + } + + @Singleton + static class Service { + + @Inject + Service1 service1 + + String get() { + return service1.get() + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/WebStubsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/WebStubsTest.java new file mode 100644 index 000000000..e97584878 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/stub/WebStubsTest.java @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.stub; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubBean; + +/** + * @author Vyacheslav Rusakov + * @since 08.02.2025 + */ +@TestDropwizardApp(value = WebStubsTest.App.class, debug = true) +public class WebStubsTest { + + @StubBean(Resource.class) + ResourceStub stub; + + @Test + void testStubbedResource(ClientSupport client) { + String res = client.get("/sample", String.class); + Assertions.assertEquals("override", res); + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Resource.class) + .build(); + } + } + + @Path("/sample") + @Produces("application/json") + public static class Resource { + + @GET + @Path("/") + public Response latest() { + return Response.ok().build(); + } + + } + + public static class ResourceStub extends Resource { + + @Override + public Response latest() { + return Response.ok("override").build(); + } + + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/AbstractTrackerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/AbstractTrackerTest.java new file mode 100644 index 000000000..e111dd8a7 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/AbstractTrackerTest.java @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import ru.vyarus.dropwizard.guice.AbstractPlatformTest; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public abstract class AbstractTrackerTest extends AbstractPlatformTest { + + @Override + protected String clean(String out) { + return out + .replaceAll("@[\\da-z]{6,10}", "@11111111") + .replaceAll("\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d+]", "[2025-22-22 11:11:11]") + .replaceAll("\\d+(\\.\\d+)? ms {4,}", "11.11 ms ") + .replaceAll("\\d+(\\.\\d+)? ms", "11.11 ms"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/CustomStringLengthTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/CustomStringLengthTest.java new file mode 100644 index 000000000..9d19d0db4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/CustomStringLengthTest.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class CustomStringLengthTest { + + @Inject + Service service; + + @TrackBean(maxStringLength = 5) + Tracker tracker; + + @Test + void testNoRowObjects() { + Assertions.assertThat(tracker).isNotNull(); + service.foo("boobooboobooboo"); + + MethodTrack track = tracker.getLastTrack(); + Assertions.assertThat(track.getArguments()).isEqualTo(new String[]{"boobo..."}); + Assertions.assertThat(track.getResult()).isEqualTo("foofo..."); + Assertions.assertThat(track.getRawArguments()).isEqualTo(new Object[]{"boobooboobooboo"}); + Assertions.assertThat(track.getRawResult()).isEqualTo("foofoofoofoofoofoofooboobooboobooboo"); + Assertions.assertThat(track.toStringTrack()).isEqualTo("foo(\"boobo...\") = \"foofo...\""); + + } + + public static class Service { + public String foo(String in) { + return "foofoofoofoofoofoofoo" + in; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/IncorrectDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/IncorrectDeclarationTest.java new file mode 100644 index 000000000..19b2d4755 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/IncorrectDeclarationTest.java @@ -0,0 +1,83 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; +import ru.vyarus.dropwizard.guice.test.track.TrackerConfig; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public class IncorrectDeclarationTest extends AbstractTrackerTest { + + @Test + void testIncorrectFieldType() { + Throwable th = runFailed(Test1.class); + Assertions.assertThat(th.getMessage()).isEqualTo( + "Field IncorrectDeclarationTest$Test1.service annotated with @TrackBean, but its type is not Tracker"); + } + + @Test + void testNoServiceType() { + + Throwable th = runFailed(Test2.class); + Assertions.assertThat(th.getMessage()).isEqualTo( + "Incorrect @TrackBean 'IncorrectDeclarationTest$Test2.service' declaration: " + + "tracked service must be declared as a tracker object generic: Tracker"); + } + + @Test + void testManualTracker() { + + Throwable th = runFailed(Test3.class); + Assertions.assertThat(th.getMessage()).contains( + "Incorrect @TrackBean 'IncorrectDeclarationTest$Test3.service' declaration: tracker instance can't be provided manually"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + @TrackBean + Service service; + + @Test + void test() { + // no matter + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test2 { + + @TrackBean + Tracker service; + + @Test + void test() { + // no matter + } + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test3 { + + @TrackBean + static Tracker service = new Tracker<>(Service.class, new TrackerConfig(), null); + + @Test + void test() { + // no matter + } + } + + public static class Service { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/InstanceTrackDetectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/InstanceTrackDetectionTest.java new file mode 100644 index 000000000..001512970 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/InstanceTrackDetectionTest.java @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +public class InstanceTrackDetectionTest extends AbstractTrackerTest{ + + @Test + void testInstanceTrackingDetection() { + + Throwable th = runFailed(Test1.class); + Assertions.assertThat(th.getMessage()).isEqualTo( + "Incorrect @TrackBean 'InstanceTrackDetectionTest$Test1.tracker' declaration: target " + + "bean 'Service' bound by instance and so can't be tracked"); + } + + @TestGuiceyApp(Test1.App.class) + @Disabled + public static class Test1 { + @Inject + Service service; + + @TrackBean(trace = true) + Tracker tracker; + + @Test + void testInstanceTracking() { + // nothing + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .modules(builder -> builder.bind(Service.class).toInstance(new Service())) + .build(); + } + } + + public static class Service { + + public String foo(int i) { + return "foo" + i; + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/NoRowObjectsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/NoRowObjectsTest.java new file mode 100644 index 000000000..bce95f5d0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/NoRowObjectsTest.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class NoRowObjectsTest { + + @Inject + Service service; + + @TrackBean(keepRawObjects = false) + Tracker tracker; + + @Test + void testNoRowObjects() { + Assertions.assertThat(tracker).isNotNull(); + service.foo(1); + + MethodTrack track = tracker.getLastTrack(); + Assertions.assertThat(track.getArguments()).isEqualTo(new String[]{"1"}); + Assertions.assertThat(track.getResult()).isEqualTo("foo1"); + Assertions.assertThat(track.getRawArguments()).isNull(); + Assertions.assertThat(track.getRawResult()).isNull(); + + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/OtherAopAppliedTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/OtherAopAppliedTest.java new file mode 100644 index 000000000..0665255f5 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/OtherAopAppliedTest.java @@ -0,0 +1,62 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import com.google.inject.matcher.Matchers; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(OtherAopAppliedTest.App.class) +public class OtherAopAppliedTest { + + @Inject + Service service; + + @TrackBean + Tracker tracker; + + @Test + void testCustomAopCounted() { + Assertions.assertThat(tracker).isNotNull(); + + // call service, intercepted with aop + Assertions.assertThat(service.foo()).isEqualTo("foo!CUSTOM!"); + // aop part must be counted + Assertions.assertThat(tracker.getLastTrack().getRawResult()).isEqualTo("foo!CUSTOM!"); + } + + public static class App extends DefaultTestApp { + + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .modules(binder -> { + binder.bindInterceptor(Matchers.only(Service.class), Matchers.any(), new CustomInterceptor()); + }) + .build(); + } + } + + public static class CustomInterceptor implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + return invocation.proceed() + "!CUSTOM!"; + } + } + + public static class Service { + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/ReportForMultipleInstancesTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/ReportForMultipleInstancesTest.java new file mode 100644 index 000000000..1b97e2451 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/ReportForMultipleInstancesTest.java @@ -0,0 +1,60 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public class ReportForMultipleInstancesTest extends AbstractTrackerTest { + + @Test + void checkSummaryWithMultipleInstances() { + + String output = run(Test1.class); + + org.assertj.core.api.Assertions.assertThat(output) + // warn + .contains("\\\\\\---[Tracker] 11.11 ms <@11111111> .foo(1) = \"foo1\"\n" + + "\\\\\\---[Tracker] 11.11 ms <@11111111> .foo(2) = \"foo2\"") + + // 2 instances + .contains("Tracker stats (sorted by median) for ReportForMultipleInstancesTest$Test1#testTracker():\n" + + "\n" + + "\t[service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] \n" + + "\tService foo(int) 2 (2) 0 11.11 ms 11.11 ms 11.11 ms 11.11 ms 11.11 ms"); + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Inject + Provider service; + + @TrackBean(trace = true, printSummary = true) + Tracker track; + + @Test + void testTracker() { + Assertions.assertNotNull(track); + service.get().foo(1); + service.get().foo(2); + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SlowMethodWarnTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SlowMethodWarnTest.java new file mode 100644 index 000000000..ec7b447fa --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SlowMethodWarnTest.java @@ -0,0 +1,86 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +import java.time.temporal.ChronoUnit; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ + +public class SlowMethodWarnTest extends AbstractTrackerTest { + + @Test + void checkSlowMethodWarning() { + + String output = run(Test1.class); + + org.assertj.core.api.Assertions.assertThat(output) + // warn + .contains("WARN [2025-22-22 11:11:11] ru.vyarus.dropwizard.guice.test.track.Tracker: \n" + + "\\\\\\---[Tracker] 11.11 ms <@11111111> .foo() = \"foo\""); + } + + @Test + void checkSlowMethodWarningDisabled() { + + String output = run(Test2.class); + + org.assertj.core.api.Assertions.assertThat(output) + .doesNotContain("WARN [2025-22-22 11:11:11] ru.vyarus.dropwizard.guice.test.track.Tracker: \n" + + "\\\\\\---[Tracker] 11.11 ms <@11111111> .foo() = \"foo\""); + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Inject Service service; + + @TrackBean(slowMethods = 1, slowMethodsUnit = ChronoUnit.MILLIS) + Tracker track; + + @Test + void testTracker() { + Assertions.assertNotNull(track); + service.foo(); + } + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled // prevent direct execution + public static class Test2 { + + @Inject Service service; + + @TrackBean(slowMethods = 0, slowMethodsUnit = ChronoUnit.MILLIS) + Tracker track; + + @Test + void testTracker() { + Assertions.assertNotNull(track); + service.foo(); + } + } + + public static class Service { + public String foo() { + try { + Thread.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SpyCompatibilityTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SpyCompatibilityTest.java new file mode 100644 index 000000000..3086b24ad --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/SpyCompatibilityTest.java @@ -0,0 +1,58 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.spy.SpyBean; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; +import ru.vyarus.dropwizard.guice.test.util.PrintUtils; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class SpyCompatibilityTest { + + @SpyBean + // this is spy (internal to AOP interceptor) + Service spy; + + @Inject + // this is a AOPed class (with both aop handlers applied) + Service realService; + + @TrackBean + Tracker tracker; + + @Test + void testSpyCompatibility() { + Assertions.assertThat(spy).isNotNull().isNotEqualTo(realService); + + realService.foo(); + + // tracked spy instance call, not guice bean + Assertions.assertThat(tracker.getLastTrack().getInstanceHash()).isEqualTo(PrintUtils.identity(spy)); + } + + @Test + void testCallingMethodsOnSpy() { + spy.foo(); + Assertions.assertThat(tracker.size()).isEqualTo(1); + Assertions.assertThat(tracker.getLastTrack().getInstanceHash()).isEqualTo(PrintUtils.identity(spy)); + + // spy actually called just once + Mockito.verify(spy, Mockito.times(1)).foo(); + } + + public static class Service { + + public String foo() { + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StaticMethodTraceTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StaticMethodTraceTest.java new file mode 100644 index 000000000..08fc4247d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StaticMethodTraceTest.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class StaticMethodTraceTest { + + @Inject + Service service; + + @TrackBean + Tracker tracker; + + @Test + void testStaticTrack() { + + service.istStatic(); + Base.intStatic(); + service.intDef(); + + Assertions.assertThat(tracker.size()).isEqualTo(1); + Assertions.assertThat(tracker.getLastTrack().toStringTrack()).isEqualTo("intDef()"); + } + + public interface Base { + default void intDef() {} + static void intStatic() {} + } + + public static class Service implements Base { + public static void istStatic() { + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StatsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StatsTest.java new file mode 100644 index 000000000..ac6b08f3a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/StatsTest.java @@ -0,0 +1,66 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; +import ru.vyarus.dropwizard.guice.test.track.stat.MethodSummary; +import ru.vyarus.dropwizard.guice.test.track.stat.TrackerStats; + +/** + * @author Vyacheslav Rusakov + * @since 19.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class StatsTest { + + @Inject + Service service; + + @TrackBean + Tracker tracker; + + @BeforeEach + void setUp() { + final TrackerStats stats = tracker.getStats(); + + Assertions.assertNotNull(stats); + Assertions.assertEquals(0, stats.getMethods().size()); + Assertions.assertNull(stats.render()); + } + + @Test + void testMethodCall() { + service.foo(); + } + + @AfterEach + void tearDown() { + final TrackerStats stats = tracker.getStats(); + + Assertions.assertNotNull(stats); + Assertions.assertEquals(1, stats.getMethods().size()); + Assertions.assertNotNull(stats.render()); + MethodSummary summary = stats.getMethods().get(0); + Assertions.assertEquals("foo", summary.getMethod().getName()); + Assertions.assertEquals(Service.class, summary.getService()); + Assertions.assertEquals(1, summary.getTracks()); + Assertions.assertEquals(0, summary.getErrors()); + Assertions.assertEquals(1, summary.getMetrics().getValues().length); + Assertions.assertNotNull(summary.getMin()); + Assertions.assertNotNull(summary.getMax()); + Assertions.assertNotNull(summary.getMedian()); + Assertions.assertNotNull(summary.get75thPercentile()); + Assertions.assertNotNull(summary.get95thPercentile()); + Assertions.assertEquals("foo() called 1 times", summary.toString()); + } + + public static class Service { + public void foo(){} + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TraceTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TraceTest.java new file mode 100644 index 000000000..857dfac13 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TraceTest.java @@ -0,0 +1,52 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public class TraceTest extends AbstractTrackerTest { + @Test + void checkTrace() { + + String output = run(Test1.class); + + org.assertj.core.api.Assertions.assertThat(output) + // warn + .contains("\\\\\\---[Tracker] 11.11 ms <@11111111> .foo(1) = \"foo1\"\n" + + "\\\\\\---[Tracker] 11.11 ms <@11111111> .foo(2) = \"foo2\""); + } + + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled // prevent direct execution + public static class Test1 { + + @Inject + Test1.Service service; + + @TrackBean(trace = true) + Tracker track; + + @Test + void testTracker() { + Assertions.assertNotNull(track); + service.foo(1); + service.foo(2); + } + + public static class Service { + public String foo(int i) { + return "foo" + i; + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackFailedMethodTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackFailedMethodTest.java new file mode 100644 index 000000000..70b3f0b67 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackFailedMethodTest.java @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +@TestGuiceyApp(DefaultTestApp.class) +public class TrackFailedMethodTest { + + @Inject + Service service; + + @TrackBean + Tracker tracker; + + @Test + void testNoRowObjects() { + Assertions.assertThat(tracker).isNotNull(); + try { + service.foo(); + Assertions.fail("Should have thrown exception"); + } catch (Exception ex) { + } + + MethodTrack track = tracker.getLastTrack(); + Assertions.assertThat(track.getArguments()).isNotNull(); + Assertions.assertThat(track.getRawArguments()).isNotNull(); + Assertions.assertThat(track.getResult()).isNull(); + Assertions.assertThat(track.getRawResult()).isNull(); + Assertions.assertThat(track.getQuotedResult()).isNull(); + Assertions.assertThat(track.getThrowable()).isNotNull(); + Assertions.assertThat(track.getThrowable().getMessage()).isEqualTo("Failed"); + Assertions.assertThat(track.toStringTrack()).isEqualTo("foo() ERROR IllegalStateException: Failed"); + + } + + public static class Service { + public void foo() { + throw new IllegalStateException("Failed"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerCleanupDisableTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerCleanupDisableTest.java new file mode 100644 index 000000000..70c637ec8 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerCleanupDisableTest.java @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public class TrackerCleanupDisableTest extends AbstractTrackerTest { + + @Test + void testNoTrackerCleanup() { + String out = run(Test1.class); + + Assertions.assertThat(out).contains("Cleanup disabled: \n" + + "\t[service] [method] [calls] [fails] [min] [max] [median] [75%] [95%] \n" + + "\tService foo() 2 (2) 0 11.11 ms 11.11 ms 11.11 ms 11.11 ms 11.11 ms"); + } + + @TestGuiceyApp(DefaultTestApp.class) + @Disabled + public static class Test1 { + + @Inject + Service service; + + @TrackBean(autoReset = false) + static Tracker tracker; + + @Test + void testNoCleanup() { + service.foo(); + } + + @Test + void testNoCleanup2() { + service.foo(); + } + + @AfterAll + static void afterAll() { + System.out.println("Cleanup disabled: \n" + tracker.getStats().render()); + } + + public static class Service { + public String foo() { + return "foo"; + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerDiagnosticCompatibilityTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerDiagnosticCompatibilityTest.java new file mode 100644 index 000000000..7014e94db --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerDiagnosticCompatibilityTest.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +/** + * @author Vyacheslav Rusakov + * @since 14.02.2025 + */ +public class TrackerDiagnosticCompatibilityTest extends AbstractTrackerTest { + + @Test + void checkTrackerWithDiagnosticEnabled() { + + String output = run(Test1.class); + + org.assertj.core.api.Assertions.assertThat(output) + // traces + .contains("\\\\\\---[Tracker] 11.11 ms <@11111111> .getNormalModuleIds() = (1)[ ItemId@11111111 ]") + + // registration debug + .contains("Applied trackers (@TrackBean) on TrackerDiagnosticCompatibilityTest$Test1:\n" + + "\n" + + "\t#track GuiceyConfigurationInfo (r.v.d.guice.module) \n" + + "\n") + + // stats report (debug) + .contains("Trackers stats (sorted by median) for TrackerDiagnosticCompatibilityTest$Test1#testTracker():\n" + + "\n" + + "\t[service] [method] [calls] [fails] [min] [max] [median] [75%] [95%]") + + .contains("GuiceyConfigurationInfo getNormalModuleIds() 1 0 11.11 ms 11.11 ms 11.11 ms 11.11 ms 11.11 ms "); + } + + + @TestGuiceyApp(value = Test1.App.class, debug = true) + @Disabled // prevent direct execution + public static class Test1 { + + @TrackBean(trace = true) + Tracker track; + + @Test + void testTracker() { + Assertions.assertNotNull(track); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .printAllGuiceBindings() + .build(); + } + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSimpleTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSimpleTest.java new file mode 100644 index 000000000..6d3c70b90 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSimpleTest.java @@ -0,0 +1,137 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track; + +import com.google.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.track.MethodTrack; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean; +import ru.vyarus.dropwizard.guice.test.track.Tracker; + +import java.util.List; + +import static org.mockito.Mockito.when; + +/** + * @author Vyacheslav Rusakov + * @since 11.02.2025 + */ +@TestGuiceyApp(value = DefaultTestApp.class, debug = true) +public class TrackerSimpleTest { + + @Inject + Service service; + + @TrackBean(trace = true) + Tracker serviceTracker; + + @Test + void testTracker() { + Assertions.assertNotNull(serviceTracker); + + // call service + Assertions.assertEquals("1 call", service.foo(1)); + + Assertions.assertEquals(Service.class, serviceTracker.getType()); + Assertions.assertEquals(1, serviceTracker.size()); + Assertions.assertEquals(1, serviceTracker.getTracks().size()); + Assertions.assertFalse(serviceTracker.isEmpty()); + MethodTrack track = serviceTracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("foo(1) = \"1 call\"")); + Assertions.assertArrayEquals(new Object[]{1}, track.getRawArguments()); + Assertions.assertArrayEquals(new String[]{"1"}, track.getArguments()); + Assertions.assertEquals("1 call", track.getRawResult()); + Assertions.assertEquals("1 call", track.getResult()); + Assertions.assertEquals("foo", track.getMethod().getName()); + Assertions.assertEquals(Service.class, track.getService()); + Assertions.assertTrue(track.getStarted() > 0); + Assertions.assertNotNull(track.getDuration()); + Assertions.assertNotNull(track.getInstanceHash()); + + + // call more + Assertions.assertEquals("2 call", service.foo(2)); + Assertions.assertEquals("1 bar", service.bar(1)); + + + Assertions.assertEquals(3, serviceTracker.getTracks().size()); + List tracks = serviceTracker.getLastTracks(2); + Assertions.assertEquals("foo(2) = \"2 call\"", tracks.get(0).toStringTrack()); + Assertions.assertEquals("bar(1) = \"1 bar\"", tracks.get(1).toStringTrack()); + + + // search with mockito api + tracks = serviceTracker.findTracks(mock -> when( + mock.foo(Mockito.anyInt())) + ); + Assertions.assertEquals(2, tracks.size()); + + // few more calls (to check mocks correct reset) + Assertions.assertEquals("foo", tracks.get(0).getMethod().getName()); + Assertions.assertEquals("foo", tracks.get(1).getMethod().getName()); + + tracks = serviceTracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + ); + Assertions.assertEquals(1, tracks.size()); + Assertions.assertEquals(1, tracks.get(0).getRawArguments()[0]); + + // and another call to make sure results not cached + Assertions.assertEquals("1 call", service.foo(1)); + + tracks = serviceTracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + ); + Assertions.assertEquals(2, tracks.size()); + } + + @Test + void testVoidMethod() { + service.baz("small"); + + MethodTrack track = serviceTracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("baz(\"small\")")); + Assertions.assertNull(track.getResult()); + Assertions.assertNull(track.getRawResult()); + Assertions.assertNull(track.getQuotedResult()); + } + + @Test + void testLargeString() { + service.baz("largelargelargelargelargelargelargelarge"); + + MethodTrack track = serviceTracker.getLastTrack(); + Assertions.assertEquals("largelargelargelargelargelarge...", track.getArguments()[0]); + } + + @Test + void testError() { + try { + service.err(11); + } catch (RuntimeException e) {} + + MethodTrack track = serviceTracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("err(11) ERROR IllegalStateException: error")); + Assertions.assertNotNull(track.getThrowable()); + Assertions.assertEquals("11", track.getArguments()[0]); + } + + public static class Service { + public String foo(int num) { + return num + " call"; + } + + public String bar(int num) { + return num + " bar"; + } + + public void baz(String in) { + } + + public String err(int in) { + throw new IllegalStateException("error"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSpockTest.groovy new file mode 100644 index 000000000..b34a74061 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/setup/track/TrackerSpockTest.groovy @@ -0,0 +1,128 @@ +package ru.vyarus.dropwizard.guice.test.jupiter.setup.track + +import com.google.inject.Inject +import org.mockito.Mockito +import ru.vyarus.dropwizard.guice.support.DefaultTestApp +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.track.MethodTrack +import ru.vyarus.dropwizard.guice.test.jupiter.ext.track.TrackBean +import ru.vyarus.dropwizard.guice.test.track.Tracker +import spock.lang.Specification + +import static org.mockito.Mockito.when + +/** + * @author Vyacheslav Rusakov + * @since 26.03.2025 + */ +@TestGuiceyApp(value = DefaultTestApp, debug = true) +class TrackerSpockTest extends Specification { + + // use java classes because too much "garbage" methods would be intercepted fpr groovy objects + @Inject + TrackerSimpleTest.Service service + + @TrackBean(trace = true) + Tracker serviceTracker + + def "Check tracker"() { + + expect: + serviceTracker + + // call service + "1 call" == service.foo(1) + + TrackerSimpleTest.Service == serviceTracker.getType() + 1 == serviceTracker.size() + 1 == serviceTracker.getTracks().size() + !serviceTracker.isEmpty() + + when: + MethodTrack track = serviceTracker.getLastTrack() + then: + track.toString().contains("foo(1) = \"1 call\"") + new Object[]{1} == track.getRawArguments() + new String[]{"1"} == track.getArguments() + "1 call" == track.getRawResult() + "1 call" == track.getResult() + "foo" == track.getMethod().getName() + TrackerSimpleTest.Service == track.getService() + track.getStarted() > 0 + track.getDuration() + track.getInstanceHash() + + + // call more + "2 call" == service.foo(2) + "1 bar" == service.bar(1) + + + 3 == serviceTracker.getTracks().size() + + when: + List tracks = serviceTracker.getLastTracks(2); + + then: + "foo(2) = \"2 call\"" == tracks.get(0).toStringTrack() + "bar(1) = \"1 bar\"" == tracks.get(1).toStringTrack() + + + // search with mockito api + when: + tracks = serviceTracker.findTracks { mock -> + when( + mock.foo(Mockito.anyInt())) + } + + then: + 2 == tracks.size() + + // few more calls (to check mocks correct reset) + "foo" == tracks.get(0).getMethod().getName() + "foo" == tracks.get(1).getMethod().getName() + + when: + tracks = serviceTracker.findTracks { mock -> + when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + } + then: + 1 == tracks.size() + 1 == tracks.get(0).getRawArguments()[0] + + // and another call to make sure results not cached + "1 call" == service.foo(1) + + when: + tracks = serviceTracker.findTracks { mock -> + when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + } + then: + 2 == tracks.size() + } + + def "Check void trace"() { + when: + service.baz("small") + then: + MethodTrack track = serviceTracker.getLastTrack() + track.toString().contains("baz(\"small\")") + track.getResult() == null + track.getRawResult() == null + track.getQuotedResult() == null + } + + def "Check error"() { + when: + service.err(11) + then: + thrown(RuntimeException) + + MethodTrack track = serviceTracker.getLastTrack() + track.toString().contains("err(11) ERROR IllegalStateException: error") + track.getThrowable() + "11" == track.getArguments()[0] + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordCompletenessTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordCompletenessTest.java new file mode 100644 index 000000000..f0b52d7ea --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordCompletenessTest.java @@ -0,0 +1,73 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.log.support.DBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.DBundleAfter; +import ru.vyarus.dropwizard.guice.test.log.support.DBundleBefore; +import ru.vyarus.dropwizard.guice.test.log.support.DManaged; +import ru.vyarus.dropwizard.guice.test.log.support.GBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.GModule; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class LogsRecordCompletenessTest { + + @Test + void testLogsRecording() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record().loggers("ru.vyarus.dropwizard.guice.test.log.support").start(Level.TRACE); + + TestSupport.build(LogRecordsApp.class) + .hooks(hook) + .runCore(injector -> { + + Assertions.assertEquals(13, logs.count()); + Assertions.assertEquals(13, logs.level(Level.TRACE).count()); + + // impossible to detect run event (bundle runs after loggers reset and before guicey run) + Assertions.assertEquals(Arrays.asList("Bundle initialized"), + logs.logger(DBundleBefore.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(DBundle1.class).messages()); + Assertions.assertEquals(logs.logger(DBundle1.class).messages().size(), + logs.logger(DBundle1.class).events().size()); + Assertions.assertEquals(logs.logger(DBundle1.class).level(Level.TRACE).count(), + logs.logger(DBundle1.class).level(Level.TRACE).count()); + Assertions.assertTrue(logs.logger(DBundle1.class).has(Level.TRACE)); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(GBundle1.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle initialized", "Bundle started"), + logs.logger(DBundleAfter.class).messages()); + + Assertions.assertEquals(Arrays.asList("Managed started"), + logs.logger(DManaged.class).messages()); + + Assertions.assertEquals(Arrays.asList("Module configured"), + logs.logger(GModule.class).messages()); + + Assertions.assertEquals(Arrays.asList("Constructor", "Before init", "After init", "Run"), + logs.logger(LogRecordsApp.class).messages()); + + Assertions.assertEquals(Arrays.asList("Bundle started", "Bundle started", "Bundle started", + "Managed started"), logs.containing("started").messages()); + + Assertions.assertEquals(Arrays.asList("Bundle started"), + logs.logger(DBundle1.class).containing("started").messages()); + Assertions.assertEquals(Arrays.asList("Bundle started"), + logs.logger(DBundle1.class).matching("Bund.+ started").messages()); + + return null; + }); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordTest.java new file mode 100644 index 000000000..b53d7f102 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/LogsRecordTest.java @@ -0,0 +1,128 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class LogsRecordTest { + + @Test + void testLogsRecording() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record().loggers(Service.class).start(Level.DEBUG); + + TestSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + + Assertions.assertNotNull(logs); + Assertions.assertEquals(2, logs.count()); + Assertions.assertEquals(logs.events().size(), logs.messages().size()); + Assertions.assertEquals(2, logs.logger(Service.class).count()); + Assertions.assertTrue(logs.has(Level.DEBUG)); + Assertions.assertTrue(logs.has(Level.INFO)); + Assertions.assertEquals(1, logs.level(Level.DEBUG).count()); + Assertions.assertEquals(1, logs.level(Level.INFO).count()); + + Service service = injector.getInstance(Service.class); + service.foo(); + Assertions.assertEquals(3, logs.count()); + Assertions.assertTrue(logs.has(Level.WARN)); + Assertions.assertEquals(1, logs.level(Level.WARN).count()); + + hook.clearLogs(); + Assertions.assertEquals(0, logs.count()); + + return null; + }); + } + + @Test + void testMultipleRecordings() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record().loggers(Service.class).start(Level.DEBUG); + final RecordedLogs logs2 = hook.record().loggers(Service.class).start(Level.DEBUG); + + TestSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + + Assertions.assertEquals(2, logs.count()); + Assertions.assertEquals(2, logs2.count()); + + Service service = injector.getInstance(Service.class); + service.foo(); + + Assertions.assertEquals(3, logs.count()); + Assertions.assertEquals(3, logs2.count()); + + return null; + }); + } + + @Test + void testRecorderDestroy() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record().loggers(Service.class).start(Level.DEBUG); + + TestSupport.build(App.class) + .hooks(hook) + .runCore(injector -> { + + Assertions.assertEquals(2, logs.count()); + + hook.clearLogs(); + hook.destroy(); + + Service service = injector.getInstance(Service.class); + service.foo(); + + Assertions.assertEquals(0, logs.count()); + + return null; + }); + } + + public static class App extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .extensions(Service.class) + .build(); + } + } + + @Singleton + public static class Service implements Managed { + private final Logger logger = LoggerFactory.getLogger(Service.class); + + public Service() { + logger.debug("Created Service {}", "smth"); + } + + @Override + public void start() throws Exception { + logger.info("Start"); + } + + @Override + public void stop() throws Exception { + logger.info("Stop"); + } + + public void foo() { + logger.warn("Foo called"); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/MixedRecorderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/MixedRecorderTest.java new file mode 100644 index 000000000..edb6b6d03 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/MixedRecorderTest.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.log.support.DBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.GBundle1; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class MixedRecorderTest { + + @Test + void testLogsRecording() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record() + .loggers(DBundle1.class) + .loggers(GBundle1.class.getName()) + .start(Level.TRACE); + + TestSupport.build(LogRecordsApp.class) + .hooks(hook) + .runCore(injector -> { + + Assertions.assertEquals(2, logs.logger(DBundle1.class).count()); + Assertions.assertEquals(2, logs.logger(GBundle1.class).count()); + + return null; + }); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/NoLevelRiseTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/NoLevelRiseTest.java new file mode 100644 index 000000000..63fa8bbc4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/NoLevelRiseTest.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.test.log; + +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.log.support.LogRecordsApp; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class NoLevelRiseTest { + + @Test + void testLogsRecording() throws Exception { + RecordLogsHook hook = new RecordLogsHook(); + final RecordedLogs logs = hook.record().start(Level.ERROR); + + TestSupport.build(LogRecordsApp.class) + .hooks(hook) + .runCore(injector -> { + + Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + Assertions.assertEquals(ch.qos.logback.classic.Level.INFO, logger.getLevel()); + Assertions.assertEquals(0, logs.count()); + return null; + }); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundle1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundle1.java new file mode 100644 index 000000000..1cd478f6f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundle1.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class DBundle1 implements ConfiguredBundle { + private final Logger logger = LoggerFactory.getLogger(DBundle1.class); + + @Override + public void initialize(Bootstrap bootstrap) { + logger.trace("Bundle initialized"); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + logger.trace("Bundle started"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleAfter.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleAfter.java new file mode 100644 index 000000000..8dbd60e10 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleAfter.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class DBundleAfter implements ConfiguredBundle { + private final Logger logger = LoggerFactory.getLogger(DBundleAfter.class); + + @Override + public void initialize(Bootstrap bootstrap) { + logger.trace("Bundle initialized"); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + logger.trace("Bundle started"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleBefore.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleBefore.java new file mode 100644 index 000000000..045d40b25 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DBundleBefore.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.ConfiguredBundle; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class DBundleBefore implements ConfiguredBundle { + private final Logger logger = LoggerFactory.getLogger(DBundleBefore.class); + + @Override + public void initialize(Bootstrap bootstrap) { + logger.trace("Bundle initialized"); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + logger.trace("Bundle started"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DManaged.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DManaged.java new file mode 100644 index 000000000..c5c9262bd --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/DManaged.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class DManaged implements Managed { + private final Logger logger = LoggerFactory.getLogger(DManaged.class); + + @Override + public void start() throws Exception { + logger.trace("Managed started"); + } + + @Override + public void stop() throws Exception { + logger.trace("Managed stopped"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GBundle1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GBundle1.java new file mode 100644 index 000000000..675549192 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GBundle1.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class GBundle1 implements GuiceyBundle { + private final Logger logger = LoggerFactory.getLogger(GBundle1.class); + + @Override + public void initialize(GuiceyBootstrap bootstrap) { + logger.trace("Bundle initialized"); + } + + @Override + public void run(GuiceyEnvironment environment) throws Exception { + logger.trace("Bundle started"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GModule.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GModule.java new file mode 100644 index 000000000..e58867a90 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/GModule.java @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import com.google.inject.Binder; +import com.google.inject.Module; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 28.02.2025 + */ +public class GModule implements Module { + private final Logger logger = LoggerFactory.getLogger(GModule.class); + + @Override + public void configure(Binder binder) { + logger.trace("Module configured"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/LogRecordsApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/LogRecordsApp.java new file mode 100644 index 000000000..49617251d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/log/support/LogRecordsApp.java @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.test.log.support; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * @author Vyacheslav Rusakov + * @since 27.02.2025 + */ +public class LogRecordsApp extends Application { + private final Logger logger = LoggerFactory.getLogger(LogRecordsApp.class); + + public LogRecordsApp() { + logger.trace("Constructor"); + } + + @Override + public void initialize(Bootstrap bootstrap) { + logger.trace("Before init"); + bootstrap.addBundle(new DBundleBefore()); + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new GBundle1()) + .dropwizardBundles(new DBundle1()) + .modules(new GModule()) + .enableAutoConfig().build()); + bootstrap.addBundle(new DBundleAfter()); + logger.trace("After init"); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + logger.trace("Run"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/mock/MocksTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/mock/MocksTest.java new file mode 100644 index 000000000..d21af21b1 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/mock/MocksTest.java @@ -0,0 +1,63 @@ +package ru.vyarus.dropwizard.guice.test.mock; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class MocksTest { + + @Test + void testMocks() throws Exception { + MocksHook hook = new MocksHook(); + final Service1 mock = hook.mock(Service1.class); + Mockito.when(mock.foo()).thenReturn("bar1"); + + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service1 service1 = injector.getInstance(Service1.class); + + Assertions.assertEquals(service1, mock); + Assertions.assertEquals("bar1", service1.foo()); + + Assertions.assertEquals(mock, hook.getMock(Service1.class)); + + hook.resetMocks(); + Assertions.assertNull(service1.foo()); + return null; + }); + } + + @Test + void testManualMock() throws Exception { + MocksHook hook = new MocksHook(); + final Service1 mock = Mockito.mock(Service1.class); + final Service1 mock2 = hook.mock(Service1.class, mock); + + Assertions.assertEquals(mock, mock2); + + Mockito.when(mock.foo()).thenReturn("bar1"); + + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service1 service1 = injector.getInstance(Service1.class); + + Assertions.assertEquals(service1, mock); + Assertions.assertEquals("bar1", service1.foo()); + return null; + }); + } + + public static class Service1 { + public String foo() { + return "foo1"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ApplicationRunCompatibilityTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ApplicationRunCompatibilityTest.java new file mode 100644 index 000000000..f221133e0 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ApplicationRunCompatibilityTest.java @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vyacheslav Rusakov + * @since 16.10.2025 + */ +public class ApplicationRunCompatibilityTest { + @Test + void testSimpleRun() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(App.class) + .hooks(rest) + .runCore(injector -> { + + assertThat(injector.getInstance(Environment.class).jersey() + .getResourceConfig().isRegistered(Resource1.class)).isTrue(); + return null; + }); + + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(Resource1.class); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ContainerSelectionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ContainerSelectionTest.java new file mode 100644 index 000000000..2f2360671 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/ContainerSelectionTest.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 21.04.2025 + */ +public class ContainerSelectionTest { + + @Test + void testContainerSelection() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .container(TestContainerPolicy.GRIZZLY) + .build(); + + IllegalStateException ex = Assertions.assertThrows(IllegalStateException.class, () -> + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore()); + + Assertions.assertEquals("org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory is not available in classpath. " + + "Add `org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2` " + + "dependency (version managed by dropwizard BOM)", ex.getMessage()); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/IgnoreExtensionTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/IgnoreExtensionTest.java new file mode 100644 index 000000000..4ac93795d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/IgnoreExtensionTest.java @@ -0,0 +1,148 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 21.04.2025 + */ +public class IgnoreExtensionTest { + + @Test + void testIgnoredExtension() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .disableResources(Resource2.class) + .disableJerseyExtensions(RestFilter2.class, RestExceptionMapper.class) + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + ErrorResource.class, + RestFilter1.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource2.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + + // exception mapper not set + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.getRestClient().get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + return null; + }); + + } + + @Test + void testIgnoreAllExtensions() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .disableAllJerseyExtensions(true) + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + Resource2.class, + ErrorResource.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + RestFilter1.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + + // exception mapper not set + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.getRestClient().get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + return null; + }); + } + + @Test + void testExactExtensions() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .resources(Resource1.class, ErrorResource.class) + .jerseyExtensions(RestFilter1.class) + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + ErrorResource.class, + RestFilter1.class, + ManagedBean.class)), + new HashSet(info.getExtensions())); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource2.class, + RestFilter2.class, + RestExceptionMapper.class, + WebFilter.class)), + new HashSet(info.getExtensionsDisabled())); + + // exception mapper not set + ProcessingException ex = Assertions.assertThrows(ProcessingException.class, + () -> rest.getRestClient().get("/error/foo", String.class)); + Assertions.assertEquals("error", ex.getCause().getMessage()); + + return null; + }); + } + + @Test + void testDisableDropwizardExtensions() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + final WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.getRestClient().get("/error/1", String.class)); + System.out.println(">>>ERROR:\n" + ex.getResponse().readEntity(String.class)); + + return null; + }); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestClientDebugTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestClientDebugTest.java new file mode 100644 index 000000000..b6b4af77e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestClientDebugTest.java @@ -0,0 +1,56 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; + +/** + * @author Vyacheslav Rusakov + * @since 09.05.2025 + */ +public class RestClientDebugTest { + + @Test + void testClientDebug() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + + String out = TestSupport.captureOutput(() -> + rest.getRestClient().get("/1/foo", String.class)); + + Assertions.assertThat(out.replaceAll("\r", "") + .replaceAll("on thread ([^\n]+)", "on thread ddd")) + .contains("[Client action]---------------------------------------------{\n" + + "1 * Sending client request on thread ddd\n" + + "1 > GET http://localhost:0/1/foo\n" + + "\n" + + "}----------------------------------------------------------\n"); + + return null; + }); + + } + + @Test + void testClientDebugDisabled() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .logRequests(false) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + + String out = TestSupport.captureOutput(() -> + rest.getRestClient().get("/1/foo", String.class)); + + Assertions.assertThat(out).doesNotContain("[Client action]---------------------------------------------{"); + + return null; + }); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestStubTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestStubTest.java new file mode 100644 index 000000000..e5a04232d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/RestStubTest.java @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.rest; + +import jakarta.ws.rs.WebApplicationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.rest.support.ErrorResource; +import ru.vyarus.dropwizard.guice.test.rest.support.ManagedBean; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource1; +import ru.vyarus.dropwizard.guice.test.rest.support.Resource2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestExceptionMapper; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter1; +import ru.vyarus.dropwizard.guice.test.rest.support.RestFilter2; +import ru.vyarus.dropwizard.guice.test.rest.support.RestStubApp; +import ru.vyarus.dropwizard.guice.test.rest.support.WebFilter; + +import java.util.Arrays; +import java.util.HashSet; + +/** + * @author Vyacheslav Rusakov + * @since 21.04.2025 + */ +public class RestStubTest { + + @Test + void testSimpleRun() throws Exception { + final RestStubsHook rest = RestStubsHook.builder() + .disableDropwizardExceptionMappers(true) + .build(); + TestSupport.build(RestStubApp.class) + .hooks(rest) + .runCore(injector -> { + GuiceyConfigurationInfo info = injector.getInstance(GuiceyConfigurationInfo.class); + + Assertions.assertEquals(new HashSet(Arrays.asList( + Resource1.class, + Resource2.class, + ErrorResource.class, + RestFilter1.class, + RestFilter2.class, + ManagedBean.class, + RestExceptionMapper.class)), + new HashSet(info.getExtensions())); + + // web extension auto disabled + Assertions.assertTrue(info.getExtensionsDisabled().contains(WebFilter.class)); + + ManagedBean managed = injector.getInstance(ManagedBean.class); + // managed called once + Assertions.assertEquals(1, managed.beforeCnt); + Assertions.assertEquals(0, managed.afterCnt); + + + String res = rest.getRestClient().get("/1/foo", String.class); + Assertions.assertEquals("foo", res); + + // rest filter used + RestFilter1 filter = injector.getInstance(RestFilter1.class); + Assertions.assertTrue(filter.called); + + + // test error + WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class, + () -> rest.getRestClient().get("/error/foo", String.class)); + RestExceptionMapper exceptionMapper = injector.getInstance(RestExceptionMapper.class); + Assertions.assertTrue(exceptionMapper.called); + Assertions.assertEquals("error", ex.getResponse().readEntity(String.class)); + + return null; + }); + + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ErrorResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ErrorResource.java new file mode 100644 index 000000000..9d27c0ce6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ErrorResource.java @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Path("/error/") +@Produces("application/json") +public class ErrorResource { + + @GET + @Path("/{foo}") + public String get(@PathParam("foo") String foo, @Context UriInfo uriInfo) { + throw new IllegalStateException("error"); + } + + @POST + @Path("/{foo}") + public void post(@PathParam("foo") String foo) { + throw new IllegalStateException("error"); + } + + @PUT + @Path("/{foo}") + public String put(@PathParam("foo") String foo) { + throw new IllegalStateException("error"); + } + + @PATCH + @Path("/{foo}") + public String patch(@PathParam("foo") String foo) { + throw new IllegalStateException("error"); + } + + @DELETE + @Path("/{foo}") + public void delete(@PathParam("foo") String foo) { + throw new IllegalStateException("error"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ManagedBean.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ManagedBean.java new file mode 100644 index 000000000..b3e3508df --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/ManagedBean.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import com.google.inject.Singleton; +import io.dropwizard.lifecycle.Managed; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Singleton +public class ManagedBean implements Managed { + + public int beforeCnt; + public int afterCnt; + + @Override + public void start() throws Exception { + beforeCnt++; + } + + @Override + public void stop() throws Exception { + afterCnt++; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource1.java new file mode 100644 index 000000000..c10c60112 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource1.java @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Path("/1/") +@Produces("application/json") +public class Resource1 { + + @GET + @Path("/{foo}") + public String get(@PathParam("foo") String foo, @Context UriInfo uriInfo) { + Preconditions.checkNotNull(uriInfo); + return foo; + } + + @POST + @Path("/{foo}") + public void post(@PathParam("foo") String foo) { + } + + @PUT + @Path("/{foo}") + public String put(@PathParam("foo") String foo) { + return foo; + } + + @PATCH + @Path("/{foo}") + public String patch(@PathParam("foo") String foo) { + return foo; + } + + @DELETE + @Path("/{foo}") + public void delete(@PathParam("foo") String foo) { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource2.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource2.java new file mode 100644 index 000000000..eea6f7060 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/Resource2.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import com.google.common.base.Preconditions; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Path("/2/") +@Produces("application/json") +public class Resource2 { + + @GET + @Path("/{foo}") + public String get(@PathParam("foo") String foo, @Context UriInfo uriInfo) { + Preconditions.checkNotNull(uriInfo); + return foo; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestExceptionMapper.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestExceptionMapper.java new file mode 100644 index 000000000..4d4b69e22 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestExceptionMapper.java @@ -0,0 +1,21 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Provider +public class RestExceptionMapper implements ExceptionMapper { + + public boolean called = false; + + @Override + public Response toResponse(Exception exception) { + called = true; + return Response.serverError().entity(exception.getMessage()).build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter1.java new file mode 100644 index 000000000..8f5a68fbe --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter1.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Provider +@Singleton +public class RestFilter1 implements ContainerResponseFilter { + + public boolean called = false; + + @Override + public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException { + called = true; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter2.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter2.java new file mode 100644 index 000000000..d3add1594 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestFilter2.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@Provider +@Singleton +public class RestFilter2 implements ContainerResponseFilter { + + public boolean called = false; + + @Override + public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException { + called = true; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestStubApp.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestStubApp.java new file mode 100644 index 000000000..bd13bbf0c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/RestStubApp.java @@ -0,0 +1,17 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +public class RestStubApp extends DefaultTestApp { + @Override + protected GuiceBundle configure() { + return GuiceBundle.builder() + .enableAutoConfig() + .build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/WebFilter.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/WebFilter.java new file mode 100644 index 000000000..fe068b422 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/rest/support/WebFilter.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.rest.support; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 22.02.2025 + */ +@jakarta.servlet.annotation.WebFilter("/*") +public class WebFilter extends HttpFilter { + + public boolean called = false; + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + called = true; + super.doFilter(req, res, chain); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseDeclarationTest.java new file mode 100644 index 000000000..c3ae304a3 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseDeclarationTest.java @@ -0,0 +1,87 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import com.google.common.truth.Truth; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 24.12.2022 + */ +public class DirectReuseDeclarationTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkDirectDeclaration() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(1, actions.size()); + Truth.assertThat(actions.get(0)).contains("can't be reused because reusable declaration must be in abstract (base)"); + } + + @TestGuiceyApp(value = NonAbstractReuseDeclarationTest.App.class, reuseApplication = true) + @Disabled // prevent direct execution + public static class Test1 { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseManualDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseManualDeclarationTest.java new file mode 100644 index 000000000..1d92ee61e --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/DirectReuseManualDeclarationTest.java @@ -0,0 +1,92 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import com.google.common.truth.Truth; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +public class DirectReuseManualDeclarationTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkDirectDeclaration() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(1, actions.size()); + Truth.assertThat(actions.get(0)).contains("can't be reused because reusable declaration must be in abstract (base)"); + } + + @Disabled // prevent direct execution + public static class Test1 { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .reuseApplication() + .create(); + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ExtensionsApiTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ExtensionsApiTest.java new file mode 100644 index 000000000..3f5adf776 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ExtensionsApiTest.java @@ -0,0 +1,129 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import com.google.common.base.Preconditions; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.GuiceyExtensionsSupport; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +public class ExtensionsApiTest { + public static List actions = new ArrayList<>(); + + @Test + void checkAppReuse() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class)) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList( + "started", "reusable1: true", "reusable1: true", "stopped", "reusable2: true", + "Error: (IllegalStateException) Guice injector not available"), actions); + Assertions.assertEquals(1, App.cnt); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @ExtendWith(Test1Ext.class) + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + public static class Test1Ext implements BeforeAllCallback, BeforeEachCallback { + @Override + public void beforeAll(ExtensionContext context) throws Exception { + actions.add("reusable1: " + GuiceyExtensionsSupport.isReusableAppUsed(context)); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + actions.add("reusable1: " + GuiceyExtensionsSupport.isReusableAppUsed(context)); + } + } + + @ExtendWith(Test2Ext.class) + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @Test + void testSample() { + } + } + + public static class Test2Ext implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) throws Exception { + Preconditions.checkState(GuiceyExtensionsSupport.isReusableAppUsed(context)); + Assertions.assertTrue(GuiceyExtensionsSupport.closeReusableApp(context)); + actions.add("reusable2: " + GuiceyExtensionsSupport.isReusableAppUsed(context)); + // still would be exception next because injector is required for fields injection + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/IncorrectFieldsUsageTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/IncorrectFieldsUsageTest.java new file mode 100644 index 000000000..d54521181 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/IncorrectFieldsUsageTest.java @@ -0,0 +1,136 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import com.google.common.truth.Truth; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; +import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; +import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; +import uk.org.webcompere.systemstubs.stream.SystemOut; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +@ExtendWith(SystemStubsExtension.class) +public class IncorrectFieldsUsageTest { + + public static List actions = new ArrayList<>(); + + @SystemStub + SystemOut out; + + @Test + void checkNotAbstractBase() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class), + selectClass(Test2.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(2, actions.size()); + + String output = out.getText().replace("\r", ""); + System.err.println(output); + Truth.assertThat(output).contains("The following extensions were used during reusable app startup in test ru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test1, but they did not belong to base class ru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Base hierarchy where reusable app is declared and so would be ignored if reusable app would start by different test: \n" + + "\tru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test1.kohook (GuiceyConfigurationHook)\n" + + "\tru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test1.kosetup (TestEnvironmentSetup)"); + + Truth.assertThat(output).contains("The following extensions were ignored in test ru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test2 because reusable application was already started by another test: \n" + + "\tru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test2.ignoredhook (GuiceyConfigurationHook)\n" + + "\tru.vyarus.dropwizard.guice.test.reuse.IncorrectFieldsUsageTest$Test2.ignoredsetup (TestEnvironmentSetup)"); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public abstract static class Base { + + @EnableSetup + static TestEnvironmentSetup oksetup = extension -> null; + @EnableHook + static GuiceyConfigurationHook okhook = builder -> {}; + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @EnableSetup + static TestEnvironmentSetup kosetup = extension -> null; + @EnableHook + static GuiceyConfigurationHook kohook = builder -> {}; + + @Test + void testSample() { + } + } + + + @Disabled // prevent direct execution + public static class Test2 extends Base { + + @EnableSetup + static TestEnvironmentSetup ignoredsetup = extension -> null; + @EnableHook + static GuiceyConfigurationHook ignoredhook = builder -> {}; + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractManualReuseDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractManualReuseDeclarationTest.java new file mode 100644 index 000000000..49f7d0409 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractManualReuseDeclarationTest.java @@ -0,0 +1,94 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 23.12.2022 + */ +public class NonAbstractManualReuseDeclarationTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkNotAbstractBase() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + } + + public static class Base { + + @RegisterExtension + static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .reuseApplication() + .create(); + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractReuseDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractReuseDeclarationTest.java new file mode 100644 index 000000000..89a1bb490 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonAbstractReuseDeclarationTest.java @@ -0,0 +1,89 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 23.12.2022 + */ +public class NonAbstractReuseDeclarationTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkNotAbstractBase() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonStaticManualDeclarationTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonStaticManualDeclarationTest.java new file mode 100644 index 000000000..b78cf13e6 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/NonStaticManualDeclarationTest.java @@ -0,0 +1,95 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import com.google.common.truth.Truth; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +public class NonStaticManualDeclarationTest { + + public static List actions = new ArrayList<>(); + + @Test + void checkNotAbstractBase() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(1, actions.size()); + Truth.assertThat(actions.get(0)).contains("Failed to find declaration field for"); + } + + public static class Base { + + @RegisterExtension + TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) + .reuseApplication() + .create(); + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableAppSpockTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableAppSpockTest.groovy new file mode 100644 index 000000000..044cadf21 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableAppSpockTest.groovy @@ -0,0 +1,116 @@ +package ru.vyarus.dropwizard.guice.test.reuse + +import com.google.inject.Injector +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.testing.DropwizardTestSupport +import org.junit.platform.engine.TestExecutionResult +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Requires +import spock.lang.Specification +import spock.util.EmbeddedSpecRunner + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +class ReusableAppSpockTest extends Specification { + static Boolean ACTIVE = false + + public static List actions = new ArrayList<>(); + + def "Check reusable app support"() { + + when: + ACTIVE = true + def runner = new EmbeddedSpecRunner() + // do not rethrow exception - all errors will remain in holder + runner.throwFailure = false + + def res = runner.runClasses(Arrays.asList(Test1, Test2)) + res.allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get() + err.printStackTrace() + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()) + }) +// .containerEvents() +// .assertStatistics(stats -> stats.failed(0).aborted(0)); + + then: + actions == ["started", "stopped"] + App.cnt == 1 + + cleanup: + ACTIVE = false + } + + @TestGuiceyApp(value = App, reuseApplication = true) + abstract static class Base extends Specification { + } + + @Requires({ ACTIVE }) + static class Test1 extends Base { + + @Inject Injector injector + + def "Test"(DropwizardTestSupport support) { + + expect: + injector != null + support == TestSupport.getContext() + } + } + + @Requires({ ACTIVE }) + static class Test2 extends Base { + + @Inject Injector injector + + def "Test"(DropwizardTestSupport support) { + + expect: + injector != null + // checking that shared support object is THE SAME + support == TestSupport.getContext() + } + } + + static class App extends Application { + + public static int cnt; + + @Override + void initialize(Bootstrap bootstrap) { + cnt++ + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started") + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped") + } + }) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableWithNestedTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableWithNestedTest.java new file mode 100644 index 000000000..c79be0edb --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/reuse/ReusableWithNestedTest.java @@ -0,0 +1,125 @@ +package ru.vyarus.dropwizard.guice.test.reuse; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.testkit.engine.EngineTestKit; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleAdapter; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStartedEvent; +import ru.vyarus.dropwizard.guice.module.lifecycle.event.jersey.ApplicationStoppedEvent; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +import jakarta.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * @author Vyacheslav Rusakov + * @since 26.12.2022 + */ +public class ReusableWithNestedTest { + public static List actions = new ArrayList<>(); + + @Test + void checkNotAbstractBase() { + EngineTestKit + .engine("junit-jupiter") + .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") + .selectors( + selectClass(Test1.class) + ) + .execute().allEvents().failed().stream() + // exceptions appended to events log + .forEach(event -> { + Throwable err = event.getPayload(TestExecutionResult.class).get().getThrowable().get(); + actions.add("Error: (" + err.getClass().getSimpleName() + ") " + err.getMessage()); + }); + + Assertions.assertEquals(Arrays.asList("started", "stopped"), actions); + } + + @TestGuiceyApp(value = App.class, reuseApplication = true) + public abstract static class Base { + } + + @Disabled // prevent direct execution + public static class Test1 extends Base { + + @Test + void testSample() { + } + + @Nested + class Level1 { + + @Inject + Environment environment; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(environment); + } + + @Nested + class Level2 { + @Inject + Environment env; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(env); + } + + @Nested + class Level3 { + + @Inject + Environment envr; + + @Test + void checkExtensionApplied() { + Assertions.assertNotNull(envr); + } + } + } + } + } + + public static class App extends Application { + + public static int cnt; + + @Override + public void initialize(Bootstrap bootstrap) { + cnt++; + bootstrap.addBundle(GuiceBundle.builder() + .listen(new GuiceyLifecycleAdapter() { + @Override + protected void applicationStarted(ApplicationStartedEvent event) { + actions.add("started"); + } + + @Override + protected void applicationStopped(ApplicationStoppedEvent event) { + actions.add("stopped"); + } + }) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyInitializerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyInitializerTest.java new file mode 100644 index 000000000..6257293f3 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyInitializerTest.java @@ -0,0 +1,54 @@ +package ru.vyarus.dropwizard.guice.test.spy; + +import io.dropwizard.lifecycle.Managed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class SpyInitializerTest { + + @Test + void testSpy() throws Exception { + SpiesHook hook = new SpiesHook(); + final SpyProxy proxy = hook.spy(Service1.class) + .withInitializer(service1 -> Mockito.doReturn("spied").when(service1).get(11)); + TestSupport.build(DefaultTestApp.class) + .hooks(hook, builder -> builder.extensions(Mng.class)) + .runCore(injector -> { + final Service1 spy = proxy.getSpy(); + + Mockito.verify(spy, Mockito.times(1)).get(11); + + Assertions.assertEquals("spied", injector.getInstance(Mng.class).res); + return null; + }); + } + + public static class Service1 { + + public String get(int id) { + return "Hello " + id; + } + } + + @Singleton + public static class Mng implements Managed { + @Inject + Service1 service1; + + public String res; + + @Override + public void start() throws Exception { + res = service1.get(11); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyTest.java new file mode 100644 index 000000000..5bf805b2f --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/spy/SpyTest.java @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.test.spy; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class SpyTest { + + @Test + void testSpy() throws Exception { + SpiesHook hook = new SpiesHook(); + final SpyProxy proxy = hook.spy(Service1.class); + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + final Service1 spy = proxy.getSpy(); + Mockito.doReturn("bar1").when(spy).get(11); + + Service1 s1 = injector.getInstance(Service1.class); + Assertions.assertEquals("bar1", s1.get(11)); + Assertions.assertEquals("Hello 10", s1.get(10)); + + Mockito.verify(spy, Mockito.times(1)).get(11); + Mockito.verify(spy, Mockito.times(1)).get(10); + + Assertions.assertEquals(spy, hook.getSpy(Service1.class)); + Assertions.assertEquals(spy, proxy.get()); + + hook.resetSpies(); + Assertions.assertEquals("Hello 11", s1.get(11)); + return null; + }); + } + + public static class Service1 { + + public String get(int id) { + return "Hello " + id; + } + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/stub/StubTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/stub/StubTest.java new file mode 100644 index 000000000..7a454928d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/stub/StubTest.java @@ -0,0 +1,104 @@ +package ru.vyarus.dropwizard.guice.test.stub; + +import com.google.common.base.Preconditions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class StubTest { + + @BeforeEach + void setUp() { + Service1Stub.created = false; + } + + @Test + void testStubs() throws Exception { + StubsHook hook = new StubsHook(); + hook.stub(Service1.class, Service1Stub.class); + + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service1 service1 = injector.getInstance(Service1.class); + + Assertions.assertInstanceOf(Service1Stub.class, service1); + Assertions.assertEquals("moon", service1.get()); + + Service1Stub stub = (Service1Stub) service1; + hook.before(); + Assertions.assertTrue(stub.beforeCalled); + hook.after(); + Assertions.assertTrue(stub.afterCalled); + + Assertions.assertEquals(stub, hook.getStub(Service1.class)); + + return null; + }); + } + + @Test + void testManualStubs() throws Exception { + StubsHook hook = new StubsHook(); + hook.stub(Service1.class, new Service1Stub()); + + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service1 service1 = injector.getInstance(Service1.class); + + Assertions.assertInstanceOf(Service1Stub.class, service1); + Assertions.assertEquals("moon", service1.get()); + + Service1Stub stub = (Service1Stub) service1; + hook.before(); + Assertions.assertTrue(stub.beforeCalled); + hook.after(); + Assertions.assertTrue(stub.afterCalled); + + return null; + }); + } + + + public static class Service1 { + + public String get() { + return "sun"; + } + } + + public static class Service1Stub extends Service1 implements StubLifecycle { + + public static boolean created; + + public boolean beforeCalled; + public boolean afterCalled; + + public Service1Stub() { + Preconditions.checkState(!created); + created = true; + } + + @Override + public String get() { + return "moon"; + } + + @Override + public void before() { + beforeCalled = true; + } + + @Override + public void after() { + afterCalled = true; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/SlowMethodTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/SlowMethodTest.java new file mode 100644 index 000000000..490dd4c49 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/SlowMethodTest.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; + +import java.time.temporal.ChronoUnit; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class SlowMethodTest { + + @Test + void testSlowMethods() throws Exception { + TrackersHook hook = new TrackersHook(); + hook.track(Service.class) + .slowMethods(1, ChronoUnit.MILLIS) + .add(); + + final String out = TestSupport.captureOutput(() -> + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + injector.getInstance(Service.class).foo(); + return null; + })); + + Assertions.assertThat(out.replace("\r", "") + .replaceAll("@[\\da-z]{6,10}", "@11111111") + .replaceAll("\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d+]", "[2025-22-22 11:11:11]") + .replaceAll("\\d+(\\.\\d+)? ms {4,}", "11.11 ms ") + .replaceAll("\\d+(\\.\\d+)? ms", "11.11 ms")) + .contains("WARN [2025-22-22 11:11:11] ru.vyarus.dropwizard.guice.test.track.Tracker: \n" + + "\\\\\\---[Tracker] 11.11 ms <@11111111> .foo() = \"foo\""); + } + + @Test + void testSlowMethodDisable() throws Exception { + TrackersHook hook = new TrackersHook(); + hook.track(Service.class) + // change default + .slowMethods(1, ChronoUnit.MILLIS) + .disableSlowMethodsLogging() + .add(); + + final String out = TestSupport.captureOutput(() -> + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + injector.getInstance(Service.class).foo(); + return null; + })); + + Assertions.assertThat(out.replace("\r", "")).doesNotContain("\\\\\\---[Tracker]"); + } + + + public static class Service { + public String foo() { + try { + Thread.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "foo"; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/TrackerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/TrackerTest.java new file mode 100644 index 000000000..8d39e46d9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/track/TrackerTest.java @@ -0,0 +1,161 @@ +package ru.vyarus.dropwizard.guice.test.track; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import ru.vyarus.dropwizard.guice.support.DefaultTestApp; +import ru.vyarus.dropwizard.guice.test.TestSupport; +import ru.vyarus.dropwizard.guice.test.track.stat.TrackerStats; + +import java.util.List; + +import static org.mockito.Mockito.when; + +/** + * @author Vyacheslav Rusakov + * @since 04.05.2025 + */ +public class TrackerTest { + + @Test + void testTracker() throws Exception { + TrackersHook hook = new TrackersHook(); + final Tracker tracker = hook.track(Service.class) + .trace(true) + .add(); + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service service = injector.getInstance(Service.class); + + // call service + Assertions.assertEquals("1 call", service.foo(1)); + + Assertions.assertEquals(Service.class, tracker.getType()); + Assertions.assertEquals(1, tracker.size()); + Assertions.assertEquals(1, tracker.getTracks().size()); + Assertions.assertFalse(tracker.isEmpty()); + MethodTrack track = tracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("foo(1) = \"1 call\"")); + Assertions.assertArrayEquals(new Object[]{1}, track.getRawArguments()); + Assertions.assertArrayEquals(new String[]{"1"}, track.getArguments()); + Assertions.assertEquals("1 call", track.getRawResult()); + Assertions.assertEquals("1 call", track.getResult()); + Assertions.assertEquals("foo", track.getMethod().getName()); + Assertions.assertEquals(Service.class, track.getService()); + Assertions.assertTrue(track.getStarted() > 0); + Assertions.assertNotNull(track.getDuration()); + Assertions.assertNotNull(track.getInstanceHash()); + + + // call more + Assertions.assertEquals("2 call", service.foo(2)); + Assertions.assertEquals("1 bar", service.bar(1)); + + + Assertions.assertEquals(3, tracker.getTracks().size()); + List tracks = tracker.getLastTracks(2); + Assertions.assertEquals("foo(2) = \"2 call\"", tracks.get(0).toStringTrack()); + Assertions.assertEquals("bar(1) = \"1 bar\"", tracks.get(1).toStringTrack()); + + + // search with mockito api + tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.anyInt())) + ); + Assertions.assertEquals(2, tracks.size()); + + // few more calls (to check mocks correct reset) + Assertions.assertEquals("foo", tracks.get(0).getMethod().getName()); + Assertions.assertEquals("foo", tracks.get(1).getMethod().getName()); + + tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + ); + Assertions.assertEquals(1, tracks.size()); + Assertions.assertEquals(1, tracks.get(0).getRawArguments()[0]); + + // and another call to make sure results not cached + Assertions.assertEquals("1 call", service.foo(1)); + + tracks = tracker.findTracks(mock -> when( + mock.foo(Mockito.intThat(argument -> argument == 1))) + ); + Assertions.assertEquals(2, tracks.size()); + + + service.baz("small"); + + track = tracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("baz(\"small\")")); + Assertions.assertNull(track.getResult()); + Assertions.assertNull(track.getRawResult()); + Assertions.assertNull(track.getQuotedResult()); + + + service.baz("largelargelargelargelargelargelargelarge"); + track = tracker.getLastTrack(); + Assertions.assertEquals("largelargelargelargelargelarge...", track.getArguments()[0]); + + + Assertions.assertThrows(IllegalStateException.class, () -> service.err(11)); + track = tracker.getLastTrack(); + Assertions.assertTrue(track.toString().contains("err(11) ERROR IllegalStateException: error")); + Assertions.assertNotNull(track.getThrowable()); + Assertions.assertEquals("11", track.getArguments()[0]); + + + final TrackerStats stats = tracker.getStats(); + Assertions.assertNotNull(stats); + Assertions.assertEquals(4, stats.getMethods().size()); + Assertions.assertNotNull(stats.render()); + + + tracker.clear(); + Assertions.assertEquals(0, tracker.size()); + + return null; + }); + } + + @Test + void resetTrackersTest() throws Exception { + TrackersHook hook = new TrackersHook(); + final Tracker tracker = hook.track(Service.class) + .trace(true) + .add(); + TestSupport.build(DefaultTestApp.class) + .hooks(hook) + .runCore(injector -> { + Service service = injector.getInstance(Service.class); + + // call service + Assertions.assertEquals("1 call", service.foo(1)); + Assertions.assertEquals(1, tracker.size()); + + hook.resetTrackers(); + Assertions.assertEquals(0, tracker.size()); + + Assertions.assertEquals(tracker, hook.getTracker(Service.class)); + + return null; + }); + } + + public static class Service { + public String foo(int num) { + return num + " call"; + } + + public String bar(int num) { + return num + " bar"; + } + + public void baz(String in) { + } + + public String err(int in) { + throw new IllegalStateException("error"); + } + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy index fc30695c2..b92f4c074 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverridePropertyRecoveryTest.groovy @@ -28,7 +28,7 @@ class ConfigOverridePropertyRecoveryTest { value1.setPrefix("test") def value2 = new ConfigOverrideExtensionValue(ExtensionContext.Namespace.GLOBAL, "bar", "bar") value2.setPrefix("test") - value2.resolveValue(new ClassContext(null, new SpecInfo())) + value2.resolveValue(new ClassContext(null, null, new SpecInfo())) GuiceyTestSupport support = new GuiceyTestSupport(AutoScanApplication.class, null, "test", value1, value2) diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverrideUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverrideUtilsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverrideUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/ConfigOverrideUtilsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/HooksUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/HooksUtilsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/HooksUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/HooksUtilsTest.groovy diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/PrintUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/PrintUtilsTest.groovy new file mode 100644 index 000000000..81edc7f93 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/test/unit/PrintUtilsTest.groovy @@ -0,0 +1,90 @@ +package ru.vyarus.dropwizard.guice.test.unit + +import ru.vyarus.dropwizard.guice.test.util.PrintUtils +import spock.lang.Specification + +import java.util.concurrent.TimeUnit + +/** + * @author Vyacheslav Rusakov + * @since 17.02.2025 + */ +class PrintUtilsTest extends Specification { + + void setup() { + Locale.setDefault(Locale.ENGLISH) + } + + def "Check toStringValue"() { + + expect: + PrintUtils.toStringValue(value, 10) == result + + where: + value | result + "longlonglonglong" | "longlonglo..." + 1 | "1" + 1.2 | "1.2" + Integer.valueOf(12) | "12" + Double.valueOf("12.1") | "12.1" + BigDecimal.valueOf(12.1) | "12.1" + Boolean.TRUE | "true" + false | "false" + null | "null" + "" | "" + [1, 2, 3] | "(3)[ 1,2,3 ]" + [] | "(0)[]" + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] | "(12)[ 1,2,3,4,5,6,7,8,9,10,... ]" + [1, 2, 3] as Object[] | "(3)[ 1,2,3 ]" + new Object() | "Object@" + PrintUtils.identity(value) + } + + def "Check identity hash"() { + + when: "construct simple object" + Object obj = new Object() + + then: "hash should be equal to default toString" + obj.toString().contains('@' + PrintUtils.identity(obj)) + + } + + def "Check metric format"() { + + expect: + PrintUtils.formatMetric(value, unit) == result + + where: + value | unit | result + 100 | null | "100.000" + 100 | TimeUnit.NANOSECONDS | "100.000 ns" + 100 | TimeUnit.MICROSECONDS | "0.100 μs" + 100 | TimeUnit.MILLISECONDS | "0.000 ms" + 100 | TimeUnit.SECONDS | "0.000 s" + 100 | TimeUnit.MINUTES | "0.000 m" + 100 | TimeUnit.HOURS | "0.000 h" + 100 | TimeUnit.DAYS | "0.000 d" + } + + def "Check nanos format"() { + expect: + PrintUtils.ms(value) == result + + where: + value | result + 5 | "0.00 ms" + 10 | "0.00001 ms" + 50 | "0.00005 ms" + 100 | "0.0001 ms" + 150 | "0.0002 ms" + 500 | "0.0005 ms" + 1_000 | "0.001 ms" + 1_500 | "0.002 ms" + 5_000 | "0.005 ms" + 10_000 | "0.01 ms" + 15_000 | "0.02 ms" + 100_000 | "0.10 ms" + 150_000 | "0.15 ms" + 1000_000 | "1.00 ms" + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy index a0909dfb0..dc95b3006 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleSupportTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.unit import com.google.common.collect.Lists -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.module.installer.CoreInstallersBundle import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy index cc3db0244..2cf2e8e7d 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/BundleTest.groovy @@ -2,9 +2,9 @@ package ru.vyarus.dropwizard.guice.unit import com.fasterxml.jackson.databind.ObjectMapper import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.injector.InjectorFactory diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ClassFiltersTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ClassFiltersTest.java new file mode 100644 index 000000000..4400b3a26 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ClassFiltersTest.java @@ -0,0 +1,106 @@ +package ru.vyarus.dropwizard.guice.unit; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Predicate; + +import static ru.vyarus.dropwizard.guice.test.util.ClassFilters.*; + +/** + * @author Vyacheslav Rusakov + * @since 28.03.2025 + */ +public class ClassFiltersTest { + + @Test + void testAnnotated() { + final Predicate> annotated = annotated(Ann1.class, Ann2.class); + Assertions.assertTrue(annotated.test(Root1.InnerAnn1.class)); + Assertions.assertTrue(annotated.test(Root1.InnerAnn2.class)); + Assertions.assertFalse(annotated.test(Root1.InnerSkip1.class)); + Assertions.assertFalse(annotated.test(Root1.InnerNoAnn.class)); + } + + @Test + void testPackages() { + final Predicate> packg = inPackages(ClassFiltersTest.class.getPackage().getName()); + Assertions.assertTrue(packg.test(Root1.class)); + Assertions.assertTrue(packg.test(Root1.InnerAnn1.class)); + Assertions.assertFalse(packg.test(GuiceyBundle.class)); + } + + @Test + void testDeclaredIn() { + final Predicate> declared = declaredIn(Root1.class); + Assertions.assertTrue(declared.test(Root1.InnerAnn1.class)); + Assertions.assertTrue(declared.test(Root1.InnerAnn2.class)); + Assertions.assertFalse(declared.test(Root2.InnerNoAnn.class)); + } + + @Test + void testIgnoreAnnotated() { + final Predicate> ignoreAnn = ignoreAnnotated(Skip1.class, Skip2.class); + Assertions.assertTrue(ignoreAnn.test(Root1.InnerAnn1.class)); + Assertions.assertFalse(ignoreAnn.test(Root1.InnerSkip1.class)); + Assertions.assertFalse(ignoreAnn.test(Root1.InnerSkip2.class)); + Assertions.assertTrue(ignoreAnn.test(Root1.InnerNoAnn.class)); + } + + @Test + void testIgnorePackages() { + final Predicate> ignorePackg = ignorePackages(ClassFiltersTest.class.getPackage().getName()); + Assertions.assertFalse(ignorePackg.test(Root1.class)); + Assertions.assertFalse(ignorePackg.test(Root1.InnerAnn1.class)); + Assertions.assertTrue(ignorePackg.test(GuiceyBundle.class)); + } + + @Test + void testIgnoreDeclaredIn() { + final Predicate> ignoreDeclared = ignoreDeclaredIn(Root1.class); + Assertions.assertFalse(ignoreDeclared.test(Root1.InnerAnn1.class)); + Assertions.assertTrue(ignoreDeclared.test(Root2.InnerNoAnn.class)); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Ann1 {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Ann2 {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Skip1 {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Skip2 {} + + public static class Root1 { + + @Ann1 + public static class InnerAnn1 {} + + @Ann2 + public static class InnerAnn2 {} + + @Skip1 + public static class InnerSkip1 {} + + @Skip2 + public static class InnerSkip2 {} + + public static class InnerNoAnn {} + } + + public static class Root2 { + public static class InnerNoAnn {} + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy index aa26fcfcb..5b4256a86 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/FeatureUtilsTest.groovy @@ -24,8 +24,8 @@ class FeatureUtilsTest extends Specification { when: "calling not allowed method" FeatureUtils.invokeMethod(FeatureUtils.findMethod(Clz, "call"), null) then: "err" - def ex = thrown(IllegalStateException) - ex.message.startsWith("Failed to invoke method") + def ex = thrown(IllegalArgumentException) + ex.message.startsWith("null object for public void ru.vyarus.dropwizard.guice.unit.FeatureUtilsTest\$Clz.call()") } static class Clz { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy index 9b15f78de..c395cd8d9 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupInstanceTest.groovy @@ -1,16 +1,16 @@ package ru.vyarus.dropwizard.guice.unit -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.ConfiguredBundle -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.ConfiguredBundle +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy similarity index 95% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy index b5efa555d..a91adcaf8 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InjectorLookupTest.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.unit import com.google.inject.Injector -import io.dropwizard.Application +import io.dropwizard.core.Application import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy index 994df1ac9..981c6cdfa 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/InstallersTest.groovy @@ -1,9 +1,11 @@ package ru.vyarus.dropwizard.guice.unit import io.dropwizard.lifecycle.Managed -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Environment import org.eclipse.jetty.util.component.LifeCycle import ru.vyarus.dropwizard.guice.AbstractTest +import ru.vyarus.dropwizard.guice.module.context.option.Options +import ru.vyarus.dropwizard.guice.module.context.option.internal.OptionsSupport import ru.vyarus.dropwizard.guice.module.installer.feature.LifeCycleInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.ManagedInstaller import ru.vyarus.dropwizard.guice.module.installer.feature.TaskInstaller @@ -18,6 +20,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.web.listener.WebListe import ru.vyarus.dropwizard.guice.module.installer.feature.web.WebServletInstaller import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller import ru.vyarus.dropwizard.guice.module.installer.install.TypeInstaller +import ru.vyarus.dropwizard.guice.module.installer.option.InstallerOptionsSupport import ru.vyarus.dropwizard.guice.support.feature.* import ru.vyarus.dropwizard.guice.support.feature.abstr.* import ru.vyarus.dropwizard.guice.support.web.feature.DummyFilter @@ -26,6 +29,7 @@ import ru.vyarus.dropwizard.guice.support.web.feature.DummyWebListener import ru.vyarus.dropwizard.guice.support.web.feature.abstr.AbstractFilter import ru.vyarus.dropwizard.guice.support.web.feature.abstr.AbstractServlet import ru.vyarus.dropwizard.guice.support.web.feature.abstr.AbstractWebListener +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils import spock.lang.Unroll /** @@ -57,13 +61,16 @@ class InstallersTest extends AbstractTest { } expect: "installer did not accept abstract class and correctly installs good one" - def inst = installer.newInstance() + def inst = InstanceUtils.create(installer) + if (InstallerOptionsSupport.isAssignableFrom(installer)) { + ((InstallerOptionsSupport) inst).setOptions(new Options(new OptionsSupport())) + } inst.matches(goodBean) !inst.matches(denyBean) if (inst instanceof TypeInstaller) inst.install(environment, goodBean) if (inst instanceof InstanceInstaller) - inst.install(environment, goodBean.newInstance()) + inst.install(environment, InstanceUtils.createWithAnyConstructor(goodBean)) where: installer | goodBean | denyBean diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/JesreySuppliersToStringTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/JesreySuppliersToStringTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/JesreySuppliersToStringTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/JesreySuppliersToStringTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PackagesValidationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PackagesValidationTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/PackagesValidationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PackagesValidationTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy index 92a27750a..dd42cdd83 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PathUtilsTest.groovy @@ -21,7 +21,7 @@ class PathUtilsTest extends Specification { '/foo/' | '/sample.txt' | '/foo/sample.txt' ' /foo/ ' | '/sample.txt' | '/foo/sample.txt' '/foo\\bar/' | '/sample.txt' | '/foo/bar/sample.txt' - 'http://foo' | 'bar' | 'http://foo/bar' + 'http://foo' | 'bar' | 'http://foo/bar' } def "Check path"() { @@ -205,4 +205,25 @@ class PathUtilsTest extends Specification { '/foo\\bar' | 'foo/bar' '/foo/bar' | 'foo/bar' } + + def "Check absolute path normalization"() { + + expect: + PathUtils.normalizeAbsolutePath(path) == res + + where: + path | res + '/' | '/' + '' | '/' + '/foo' | '/foo' + ' /foo/ ' | '/foo' + '/foo\\bar' | '/foo/bar' + '/foo/bar' | '/foo/bar' + '/foo/bar?some=1' | '/foo/bar?some=1' + '/foo/bar#!some=1' | '/foo/bar#!some=1' + 'http://localhost.com/foo/bar/' | 'http://localhost.com/foo/bar' + 'HTTP://localhost.com/foo/bar/' | 'HTTP://localhost.com/foo/bar' + 'http://localhost.com' | 'http://localhost.com' + 'http://localhost.com:8080' | 'http://localhost.com:8080' + } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PropertyUtilsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PropertyUtilsTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/PropertyUtilsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/PropertyUtilsTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy index 10207e643..d40d1ad2b 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/unit/ReflectionHelperJarScanTest.groovy @@ -13,7 +13,7 @@ class ReflectionHelperJarScanTest extends Specification { def "Check jar scan"() { when: "scan jars" - List classes = OReflectionHelper.getClassesFor("io.dropwizard.cli", Thread.currentThread().getContextClassLoader()) + List classes = OReflectionHelper.getClassesFor("io.dropwizard.core.cli", Thread.currentThread().getContextClassLoader()) then: "classes found" classes.size() == 6 } diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/AppUrlBuilderTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/AppUrlBuilderTest.java new file mode 100644 index 000000000..6f94fa643 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/AppUrlBuilderTest.java @@ -0,0 +1,366 @@ +package ru.vyarus.dropwizard.guice.url; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.dw.ClientSupportDwTest; +import ru.vyarus.dropwizard.guice.test.jupiter.param.Jit; + +import java.io.IOException; +import java.io.PrintWriter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Vyacheslav Rusakov + * @since 30.09.2025 + */ +public class AppUrlBuilderTest { + + interface ClientCallTest { + @Test + default void callClient(ClientSupport client, @Jit AppUrlBuilder builder) { + assertEquals("main", client.target(builder.app("servlet")) + .request().buildGet().invoke().readEntity(String.class)); + + assertEquals("admin", client.target(builder.admin("servlet")) + .request().buildGet().invoke().readEntity(String.class)); + + assertEquals("ok", client.target(builder.rest("sample")).request().buildGet().invoke(String.class)); + assertEquals("ok", client.target(builder.rest("sub1/ok")).request().buildGet().invoke(String.class)); + assertEquals("ok", client.target(builder.rest("sub2/gg/ok")).request().buildGet().invoke(String.class)); + + assertEquals("ok", client.target(builder.rest(Resource.class) + .method(Resource::sample).build()) + .request().buildGet().invoke(String.class)); + assertEquals("ok", client.target(builder.rest(Resource.class) + .method(instance -> instance.sub().sample()).build()) + .request().buildGet().invoke(String.class)); + assertEquals("ok", client.target(builder.rest(Resource.class) + .method(instance -> instance.sub("gg").sample()).build()) + .request().buildGet().invoke(String.class)); + } + } + + @TestDropwizardApp(App.class) + @Nested + class DefaultConfig implements ClientCallTest { + + @Test + void testClient(@Jit AppUrlBuilder builder) { + assertEquals("http://localhost:8080/", builder.root("/")); + assertEquals("http://localhost:8080/", builder.app("/")); + assertEquals("http://localhost:8081/", builder.admin("/")); + assertEquals("http://localhost:8080/", builder.rest("/")); + assertEquals("http://localhost:8080/sample", builder.rest(Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/sample", builder.rest(uriBuilder -> uriBuilder.path("/"), Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/sample", builder.rest(Resource.class).method("sample").build()); + assertEquals("http://localhost:8080/sample/nm", builder.rest(Resource.class).method("sample", String.class).pathParam("name", "nm").build()); + assertEquals("http://localhost:8080/sample/{name}", builder.rest(Resource.class).method("sample", String.class).buildTemplate()); + assertEquals("http://localhost:8080/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).build()); + assertEquals("http://localhost:8080/sample/q?q=1", builder.rest(Resource.class).method("sample", Integer.class).queryParam("q", 1).build()); + assertEquals("http://localhost:8080/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).method("sample", Integer.class) + .queryParam("q", 1).queryParam("p", 2, 3).build()); + assertEquals("http://localhost:8080/matrix/m;m=1;p=2;p=3", builder.rest(Resource.class).method("matrix") + .matrixParam("m", 1).matrixParam("p", 2, 3).build()); + + assertEquals("http://localhost:8080/sample/q?q=1", builder.rest(Resource.class).path("sample/q?q=%s", 1).build()); + assertEquals("http://localhost:8080/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).path("sample/q?q=%s&p=%s&p=%s", 1, 2, 3).build()); + + assertEquals("http://localhost:8080/sub1/ok", builder.rest(Resource.class) + .subResource("sub1", SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/sub1/ok", builder.rest(Resource.class) + .subResource(Resource::sub, SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/sub1/ok", builder.rest(Resource.class) + .method(instance -> instance.sub().sample()).build()); + assertEquals("http://localhost:8080/sub2/gg/ok", builder.rest(Resource.class) + .method(instance -> instance.sub("gg").sample()).build()); + + assertEquals("http://localhost:8080/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).buildUri().toString()); + + AppUrlBuilder hosted = builder.forHost("https://some.com"); + assertEquals("https://some.com:8080/", hosted.root("/")); + assertEquals("https://some.com:8080/", hosted.app("/")); + assertEquals("https://some.com:8081/", hosted.admin("/")); + assertEquals("https://some.com:8080/", hosted.rest("/")); + assertEquals("https://some.com:8080/sample", hosted.rest(Resource.class).method(Resource::sample).build()); + + AppUrlBuilder proxied = builder.forProxy("https://some.com/app"); + assertEquals("https://some.com/app/", proxied.root("/")); + assertEquals("https://some.com/app/", proxied.app("/")); + assertEquals("https://some.com/app/", proxied.admin("/")); + assertEquals("https://some.com/app/", proxied.rest("/")); + assertEquals("https://some.com/app/sample", proxied.rest(Resource.class).method(Resource::sample).build()); + } + } + + @TestDropwizardApp(value = App.class, randomPorts = true) + @Nested + class DefaultCustomPortsConfig implements ClientCallTest { + + @Test + void testClient(@Jit AppUrlBuilder builder) { + Assertions.assertNotEquals(8080, builder.getAppPort()); + Assertions.assertNotEquals(8081, builder.getAdminPort()); + + assertEquals("http://localhost:" + builder.getAppPort() + "/", builder.root("/")); + assertEquals("http://localhost:" + builder.getAppPort() + "/", builder.app("/")); + assertEquals("http://localhost:" + builder.getAdminPort() + "/", builder.admin("/")); + assertEquals("http://localhost:" + builder.getAppPort() + "/", builder.rest("/")); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample", builder.rest(Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample", builder.rest(uriBuilder -> uriBuilder.path("/") ,Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample", builder.rest(Resource.class).method("sample").build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/nm", builder.rest(Resource.class).method("sample", String.class).pathParam("name", "nm").build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/{name}", builder.rest(Resource.class).method("sample", String.class).buildTemplate()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=1", builder.rest(Resource.class).method("sample", Integer.class).queryParam("q", 1).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).method("sample", Integer.class) + .queryParam("q", 1).queryParam("p", 2, 3).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/matrix/m;m=1;p=2;p=3", builder.rest(Resource.class).method("matrix") + .matrixParam("m", 1).matrixParam("p", 2, 3).build()); + + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=1", builder.rest(Resource.class).path("sample/q?q=%s", 1).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).path("sample/q?q=%s&p=%s&p=%s", 1, 2, 3).build()); + + assertEquals("http://localhost:" + builder.getAppPort() + "/sub1/ok", builder.rest(Resource.class) + .subResource("sub1", SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sub1/ok", builder.rest(Resource.class) + .subResource(Resource::sub, SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sub1/ok", builder.rest(Resource.class) + .method(instance -> instance.sub().sample()).build()); + assertEquals("http://localhost:" + builder.getAppPort() + "/sub2/gg/ok", builder.rest(Resource.class) + .method(instance -> instance.sub("gg").sample()).build()); + + assertEquals("http://localhost:" + builder.getAppPort() + "/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).buildUri().toString()); + + AppUrlBuilder hosted = builder.forHost("https://some.com"); + assertEquals("https://some.com:" + builder.getAppPort() + "/", hosted.root("/")); + assertEquals("https://some.com:" + builder.getAppPort() + "/", hosted.app("/")); + assertEquals("https://some.com:" + builder.getAdminPort() + "/", hosted.admin("/")); + assertEquals("https://some.com:" + builder.getAppPort() + "/", hosted.rest("/")); + assertEquals("https://some.com:" + +builder.getAppPort() + "/sample", hosted.rest(Resource.class).method(Resource::sample).build()); + + AppUrlBuilder proxied = builder.forProxy("https://some.com/app"); + assertEquals("https://some.com/app/", proxied.root("/")); + assertEquals("https://some.com/app/", proxied.app("/")); + assertEquals("https://some.com/app/", proxied.admin("/")); + assertEquals("https://some.com/app/", proxied.rest("/")); + assertEquals("https://some.com/app/sample", proxied.rest(Resource.class).method(Resource::sample).build()); + } + } + + + @TestDropwizardApp(value = App.class, configOverride = { + "server.applicationContextPath: /app", + "server.adminContextPath: /admin", + }, restMapping = "api") + @Nested + class ChangedDefaultConfig implements ClientCallTest { + + @Test + void testClient(@Jit AppUrlBuilder builder) { + assertEquals("http://localhost:8080/", builder.root("/")); + assertEquals("http://localhost:8080/app/", builder.app("/")); + assertEquals("http://localhost:8081/admin/", builder.admin("/")); + assertEquals("http://localhost:8080/app/api/", builder.rest("/")); + assertEquals("http://localhost:8080/app/api/sample", builder.rest(Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/app/api/sample", builder.rest(uriBuilder -> uriBuilder.path("/"), Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/app/api/sample", builder.rest(Resource.class).method("sample").build()); + assertEquals("http://localhost:8080/app/api/sample/nm", builder.rest(Resource.class).method("sample", String.class).pathParam("name", "nm").build()); + assertEquals("http://localhost:8080/app/api/sample/{name}", builder.rest(Resource.class).method("sample", String.class).buildTemplate()); + assertEquals("http://localhost:8080/app/api/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).build()); + assertEquals("http://localhost:8080/app/api/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).method("sample", Integer.class) + .queryParam("q", 1).queryParam("p", 2, 3).build()); + assertEquals("http://localhost:8080/app/api/matrix/m;m=12", builder.rest(Resource.class).method(res -> res.matrix(12)).build()); + assertEquals("http://localhost:8080/app/api/matrix/m;m=1;p=2;p=3", builder.rest(Resource.class).method("matrix") + .matrixParam("m", 1).matrixParam("p", 2, 3).build()); + + + assertEquals("http://localhost:8080/app/api/sample/q?q=1", builder.rest(Resource.class).path("sample/q?q=%s", 1).build()); + assertEquals("http://localhost:8080/app/api/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).path("sample/q?q=%s&p=%s&p=%s", 1, 2, 3).build()); + + assertEquals("http://localhost:8080/app/api/sub1/ok", builder.rest(Resource.class) + .subResource("sub1", SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/app/api/sub1/ok", builder.rest(Resource.class) + .subResource(Resource::sub, SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/app/api/sub1/ok", builder.rest(Resource.class) + .method(instance -> instance.sub().sample()).build()); + assertEquals("http://localhost:8080/app/api/sub2/gg/ok", builder.rest(Resource.class) + .method(instance -> instance.sub("gg").sample()).build()); + + assertEquals("http://localhost:8080/app/api/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).buildUri().toString()); + + AppUrlBuilder hosted = builder.forHost("https://some.com"); + assertEquals("https://some.com:8080/", hosted.root("/")); + assertEquals("https://some.com:8080/app/", hosted.app("/")); + assertEquals("https://some.com:8081/admin/", hosted.admin("/")); + assertEquals("https://some.com:8080/app/api/", hosted.rest("/")); + assertEquals("https://some.com:8080/app/api/sample", hosted.rest(Resource.class).method(Resource::sample).build()); + + AppUrlBuilder proxied = builder.forProxy("https://some.com/v1"); + assertEquals("https://some.com/v1/", proxied.root("/")); + assertEquals("https://some.com/v1/app/", proxied.app("/")); + assertEquals("https://some.com/v1/admin/", proxied.admin("/")); + assertEquals("https://some.com/v1/app/api/", proxied.rest("/")); + assertEquals("https://some.com/v1/app/api/sample", proxied.rest(Resource.class).method(Resource::sample).build()); + } + } + + @TestDropwizardApp(value = App.class, config = "src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml") + @Nested + class SimpleConfig implements ClientCallTest { + + @Test + void testClient(@Jit AppUrlBuilder builder) { + assertEquals("http://localhost:8080/", builder.root("/")); + assertEquals("http://localhost:8080/", builder.app("/")); + assertEquals("http://localhost:8080/admin/", builder.admin("/")); + assertEquals("http://localhost:8080/rest/", builder.rest("/")); + assertEquals("http://localhost:8080/rest/sample", builder.rest(Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/rest/sample", builder.rest(uriBuilder -> uriBuilder.path("/"), Resource.class).method(Resource::sample).build()); + assertEquals("http://localhost:8080/rest/sample", builder.rest(Resource.class).method("sample").build()); + assertEquals("http://localhost:8080/rest/sample/nm", builder.rest(Resource.class).method("sample", String.class).pathParam("name", "nm").build()); + assertEquals("http://localhost:8080/rest/sample/{name}", builder.rest(Resource.class).method("sample", String.class).buildTemplate()); + assertEquals("http://localhost:8080/rest/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).build()); + assertEquals("http://localhost:8080/rest/sample/q?q=1", builder.rest(Resource.class).method("sample", Integer.class).queryParam("q", 1).build()); + assertEquals("http://localhost:8080/rest/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).method("sample", Integer.class) + .queryParam("q", 1).queryParam("p", 2, 3).build()); + assertEquals("http://localhost:8080/rest/matrix/m;m=1;p=2;p=3", builder.rest(Resource.class).method("matrix") + .matrixParam("m", 1).matrixParam("p", 2, 3).build()); + + assertEquals("http://localhost:8080/rest/sample/q?q=1", builder.rest(Resource.class).path("sample/q?q=%s", 1).build()); + assertEquals("http://localhost:8080/rest/sample/q?q=1&p=2&p=3", builder.rest(Resource.class).path("sample/q?q=%s&p=%s&p=%s", 1, 2, 3).build()); + + assertEquals("http://localhost:8080/rest/sample/q?q=12", builder.rest(Resource.class).method(res -> res.sample(12)).buildUri().toString()); + + assertEquals("http://localhost:8080/rest/sub1/ok", builder.rest(Resource.class) + .subResource("sub1", SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/rest/sub1/ok", builder.rest(Resource.class) + .subResource(Resource::sub, SubResource.class).method(SubResource::sample).build()); + assertEquals("http://localhost:8080/rest/sub1/ok", builder.rest(Resource.class) + .method(instance -> instance.sub().sample()).build()); + assertEquals("http://localhost:8080/rest/sub2/gg/ok", builder.rest(Resource.class) + .method(instance -> instance.sub("gg").sample()).build()); + + AppUrlBuilder hosted = builder.forHost("https://some.com"); + assertEquals("https://some.com:8080/", hosted.root("/")); + assertEquals("https://some.com:8080/", hosted.app("/")); + assertEquals("https://some.com:8080/admin/", hosted.admin("/")); + assertEquals("https://some.com:8080/rest/", hosted.rest("/")); + assertEquals("https://some.com:8080/rest/sample", hosted.rest(Resource.class).method(Resource::sample).build()); + + AppUrlBuilder proxied = builder.forProxy("https://some.com/v1"); + assertEquals("https://some.com/v1/", proxied.root("/")); + assertEquals("https://some.com/v1/", proxied.app("/")); + assertEquals("https://some.com/v1/admin/", proxied.admin("/")); + assertEquals("https://some.com/v1/rest/", proxied.rest("/")); + assertEquals("https://some.com/v1/rest/sample", proxied.rest(Resource.class).method(Resource::sample).build()); + } + } + + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(Resource.class); + environment.servlets().addServlet("test", new ClientSupportDwTest.Servlet(false)) + .addMapping("/servlet"); + environment.admin().addServlet("testAdmin", new ClientSupportDwTest.Servlet(true)) + .addMapping("/servlet"); + } + } + + @Path("/") + public static class Resource { + + @GET + @Path("/sample/") + public String sample() { + return "ok"; + } + + @GET + @Path("/sample/{name}") + public String sample(@PathParam("name") String name) { + return "ok"; + } + + @GET + @Path("/sample/q") + public String sample(@QueryParam("q") Integer q) { + return "ok"; + } + + @GET + @Path("/matrix/m") + public String matrix(@MatrixParam("m") Integer q) { + return "ok"; + } + + @Path("/sub1/") + public SubResource sub() { + return new SubResource(); + } + + @Path("/sub2/{custom}") + public CustomSubResource sub(@PathParam("custom") String custom) { + return new CustomSubResource(); + } + } + + public static class SubResource { + @GET + @Path("/ok/") + public String sample() { + return "ok"; + } + } + + // NOTE: sub-resource path is IGNORED + @Path("/ss") + public static class CustomSubResource { + @GET + @Path("/ok/") + public String sample() { + return "ok"; + } + } + + public static class Servlet extends HttpServlet { + + private boolean admin; + + public Servlet(boolean admin) { + this.admin = admin; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + PrintWriter writer = resp.getWriter(); + writer.write(admin ? "admin" : "main"); + writer.flush(); + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzerTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzerTest.java new file mode 100644 index 000000000..c9ffafef5 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/ResourceAnalyzerTest.java @@ -0,0 +1,283 @@ +package ru.vyarus.dropwizard.guice.url.resource; + +import jakarta.ws.rs.core.MediaType; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.url.model.ResourceMethodInfo; +import ru.vyarus.dropwizard.guice.url.resource.support.DirectResource; +import ru.vyarus.dropwizard.guice.url.resource.support.InterfaceResource; +import ru.vyarus.dropwizard.guice.url.resource.support.MappedBean; +import ru.vyarus.dropwizard.guice.url.resource.support.NoPathResource; +import ru.vyarus.dropwizard.guice.url.resource.support.ResourceDeclaration; +import ru.vyarus.dropwizard.guice.url.resource.support.SuperclassDeclarationResource; +import ru.vyarus.dropwizard.guice.url.resource.support.sub.RootResource; +import ru.vyarus.dropwizard.guice.url.resource.support.sub.SubResource1; +import ru.vyarus.dropwizard.guice.url.resource.support.sub.SubResource2; + +import java.io.InputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +public class ResourceAnalyzerTest { + + @Test + void testResourcePath() { + assertThat(ResourceAnalyzer.getResourcePath(DirectResource.class)) + .isEqualTo("/direct"); + assertThat(ResourceAnalyzer.getResourcePath(SuperclassDeclarationResource.class)) + .isEqualTo("/direct"); + assertThat(ResourceAnalyzer.getResourcePath(InterfaceResource.class)) + .isEqualTo("/iface"); + } + + @Test + void testMethodPath() throws Exception { + assertThat(ResourceAnalyzer.getMethodPath(DirectResource.class, "form")).isEqualTo("/form"); + assertThat(ResourceAnalyzer.getMethodPath(SuperclassDeclarationResource.class, "form")).isEqualTo("/form"); + assertThat(ResourceAnalyzer.getMethodPath(InterfaceResource.class, "form")).isEqualTo("/form"); + + assertThat(ResourceAnalyzer.getMethodPath(DirectResource.class.getMethod("get", MappedBean.class))) + .isEqualTo("/{sm}/2"); + + // NO @Path on method + assertThat(ResourceAnalyzer.getMethodPath(DirectResource.class, "nopath")).isEqualTo("/"); + assertThat(ResourceAnalyzer.getMethodPath(InterfaceResource.class, "nopath")).isEqualTo("/"); + } + + @Test + void testAnnotatedResourceSearch() { + assertThat(ResourceAnalyzer.findAnnotatedResource(DirectResource.class)) + .isEqualTo(DirectResource.class); + assertThat(ResourceAnalyzer.findAnnotatedResource(SuperclassDeclarationResource.class)) + .isEqualTo(DirectResource.class); + assertThat(ResourceAnalyzer.findAnnotatedResource(InterfaceResource.class)) + .isEqualTo(ResourceDeclaration.class); + + assertThatThrownBy(() -> ResourceAnalyzer.findAnnotatedResource(NoPathResource.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("@Path annotation was not found on resource NoPathResource or any of it's super classes and interfaces"); + } + + @Test + void testAnnotatedMethodSearch() throws Exception { + assertThat(ResourceAnalyzer.findAnnotatedMethod(DirectResource.class.getMethod("get", MappedBean.class)).getDeclaringClass()) + .isEqualTo(DirectResource.class); + assertThat(ResourceAnalyzer.findAnnotatedMethod(SuperclassDeclarationResource.class.getMethod("get", MappedBean.class)).getDeclaringClass()) + .isEqualTo(DirectResource.class); + assertThat(ResourceAnalyzer.findAnnotatedMethod(InterfaceResource.class.getMethod("get", MappedBean.class)).getDeclaringClass()) + .isEqualTo(ResourceDeclaration.class); + } + + @Test + void testMethodCallAnalysis() throws Exception { + // WHEN analyze get + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res.get("1", "2", "3", "4", "5")); + + assertThat(info.getResource()).isEqualTo(DirectResource.class); + assertThat(info.getResourcePath()).isEqualTo("/direct"); + assertThat(info.getMethod()) + .isEqualTo(DirectResource.class.getMethod("get", String.class, String.class, String.class, String.class, String.class)); + assertThat(info.getSubResources()).isEmpty(); + assertThat(info.getSteps()).isEmpty(); + + assertThat(info.getPath()).isEqualTo("/{sm}"); + assertThat(info.getFullPath()).isEqualTo("/direct/{sm}"); + assertThat(info.getHttpMethod()).isEqualTo("GET"); + assertThat(info.getProduces()).contains(MediaType.APPLICATION_JSON); + assertThat(info.getConsumes()).isEmpty(); + + assertThat(info.getPathParams()).isNotEmpty().containsEntry("sm", "1"); + assertThat(info.getQueryParams()).isNotEmpty().containsEntry("q", "2"); + assertThat(info.getHeaderParams()).isNotEmpty().containsEntry("HH", "3"); + assertThat(info.getCookieParams()).isNotEmpty().containsEntry("cc", "4"); + assertThat(info.getMatrixParams()).isNotEmpty().containsEntry("mm", "5"); + assertThat(info.toString()).isEqualTo("DirectResource.Response get(String, String, String, String, String) (/direct/{sm})"); + + + // WHEN null values provided + info = ResourceAnalyzer.analyzeMethodCall(DirectResource.class, res -> res.get(null, null, null, null, null)); + assertThat(info.getPathParams()).isEmpty(); + assertThat(info.getQueryParams()).isEmpty(); + assertThat(info.getHeaderParams()).isEmpty(); + assertThat(info.getCookieParams()).isEmpty(); + assertThat(info.getMatrixParams()).isEmpty(); + + // WHEN method without path annotation + info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, DirectResource::nopath); + assertThat(info.getHttpMethod()).isEqualTo("GET"); + assertThat(info.getPath()).isEqualTo("/"); + } + + @Test + void testMethodCallChainAnalysis() throws Exception { + // WHEN analyze get + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(RootResource.class, res -> res.sub1().sub2("x").get("1")); + + assertThat(info.getResource()).isEqualTo(RootResource.class); + assertThat(info.getResourcePath()).isEqualTo("/root"); + assertThat(info.getMethod()) + .isEqualTo(SubResource2.class.getMethod("get", String.class)); + assertThat(info.getSubResources()) + .hasSize(2) + .contains(SubResource1.class, SubResource2.class); + assertThat(info.getSteps()).hasSize(3); + + + assertThat(info.getPath()).isEqualTo("/sub1/sub2/{name}/{sm}"); + assertThat(info.getFullPath()).isEqualTo("/root/sub1/sub2/{name}/{sm}"); + assertThat(info.getHttpMethod()).isEqualTo("GET"); + assertThat(info.getProduces()).contains(MediaType.APPLICATION_JSON); + assertThat(info.getConsumes()).contains(MediaType.TEXT_PLAIN); + + assertThat(info.getPathParams()).hasSize(2) + .containsEntry("name", "x") + .containsEntry("sm", "1"); + assertThat(info.getQueryParams()).isEmpty(); + assertThat(info.getHeaderParams()).isEmpty(); + assertThat(info.getCookieParams()).isEmpty(); + } + + @Test + void testRawEntityRecognition() { + // WHEN analyze simple post with entity + final DirectResource.ModelType entity = new DirectResource.ModelType("test"); + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res.post2(entity)); + + assertThat(info.getEntity()).isEqualTo(entity); + + // WHEN analyze simple post with annotated + info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res.post3(entity)); + + assertThat(info.getEntity()).isEqualTo(entity); + } + + @Test + void testBeanParamMapping() { + MappedBean bean = new MappedBean(); + bean.setSm("1"); + bean.setQ("2"); + bean.setHh("3"); + bean.setCc("4"); + bean.setMm("5"); + + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res.get(bean)); + + assertThat(info.getPathParams()).isNotEmpty().containsEntry("sm", "1"); + assertThat(info.getQueryParams()).isNotEmpty().containsEntry("q", "2"); + assertThat(info.getHeaderParams()).isNotEmpty().containsEntry("HH", "3"); + assertThat(info.getCookieParams()).isNotEmpty().containsEntry("cc", "4"); + assertThat(info.getMatrixParams()).isNotEmpty().containsEntry("mm", "5"); + } + + @Test + void testSimpleForm() { + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res.form("1", 2)); + + assertThat(info.getFormParams()).isNotEmpty() + .containsEntry("p1", "1") + .containsEntry("p2", 2); + assertThat(info.getConsumes()).isNotEmpty() + .contains(MediaType.APPLICATION_FORM_URLENCODED); + } + + @Test + void testMultipartForm() { + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res + .multipart("1", getClass().getResourceAsStream("/logback.xml"), new FormDataBodyPart("p2", "2"))); + + assertThat(info.getFormParams()).isNotEmpty() + .containsEntry("p1", "1") + .containsEntry("p2", "2"); + assertThat(info.getConsumes()).isNotEmpty() + .contains(MediaType.MULTIPART_FORM_DATA); + } + + @Test + void testMultipartForm2() { + InputStream file = getClass().getResourceAsStream("/logback.xml"); + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res + .multipart2("1", file, new FormDataContentDisposition("form-data; name=\"file\"; filename=\"filename.jpg\""))); + + assertThat(info.getFormParams()).isNotEmpty() + .containsEntry("p1", "1") + .extracting(map -> map.get("file")) + .isInstanceOf(StreamDataBodyPart.class) + .asInstanceOf(InstanceOfAssertFactories.type(StreamDataBodyPart.class)) + .extracting(StreamDataBodyPart::getStreamEntity, StreamDataBodyPart::getFilename) + .containsExactly(file, "filename.jpg"); + assertThat(info.getConsumes()).isNotEmpty() + .contains(MediaType.MULTIPART_FORM_DATA); + + + info = ResourceAnalyzer + .analyzeMethodCall(DirectResource.class, res -> res + .multipart2("1", file, null)); //no file name + + assertThat(info.getFormParams()).isNotEmpty() + .containsEntry("p1", "1") + .containsEntry("file", file); + assertThat(info.getConsumes()).isNotEmpty() + .contains(MediaType.MULTIPART_FORM_DATA); + } + + @Test + void testAnnotated() { + ResourceMethodInfo info = ResourceAnalyzer + .analyzeMethodCall(InterfaceResource.class, res -> res.get(null, null, null, null)); + + assertThat(info.getResource()).isEqualTo(ResourceDeclaration.class); + assertThat(info.getMethod().getDeclaringClass()).isEqualTo(ResourceDeclaration.class); + } + + @Test + void testErrors() { + assertThatThrownBy(() -> ResourceAnalyzer.analyzeMethodCall(null, res -> {})) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Resource class is required"); + + assertThatThrownBy(() -> ResourceAnalyzer.analyzeMethodCall(DirectResource.class, res -> {})) + .isInstanceOf(IllegalStateException.class) + .hasMessage("No method calls recorded"); + + assertThatThrownBy(() -> ResourceAnalyzer.analyzeMethodCall(DirectResource.class, res -> { + throw new IllegalStateException("fail"); + })) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to record method call on resource 'DirectResource'"); + + assertThatThrownBy(() -> ResourceAnalyzer.findMethod(DirectResource.class, "unknown")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Method 'unknown' not found in class 'DirectResource'"); + + assertThatThrownBy(() -> ResourceAnalyzer.findMethod(DirectResource.class, "unknown", String.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Method 'unknown(String)' not found in class 'DirectResource'"); + + assertThatThrownBy(() -> ResourceAnalyzer.findMethod(DirectResource.class, "get")) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith("Method with name 'get' is not unique in class 'DirectResource'") + .hasMessageContaining("Response get(MappedBean)") + .hasMessageContaining("Response get(String, String, String, String, String)"); + + assertThatThrownBy(() -> ResourceAnalyzer.findHttpMethod(RootResource.class.getMethod("sub1"))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Http method type annotation was not found on resource method: SubResource1 sub1()"); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/DirectResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/DirectResource.java new file mode 100644 index 000000000..d585bdba9 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/DirectResource.java @@ -0,0 +1,104 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.InputStream; + +/** + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +@Path("/direct/") +public class DirectResource { + + @GET + @Path("/{sm}/") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("sm") String sm, + @QueryParam("q") String q, + @HeaderParam("HH") String hh, + @CookieParam("cc") String cc, + @MatrixParam("mm") String mm) { + return Response.ok().build(); + } + + @GET + public String nopath() { + return "nopath"; + } + + @GET + @Path("/{sm}/2") + public Response get(@BeanParam MappedBean bean) { + return Response.ok().build(); + } + + @POST + @Path("/form") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public void form(@FormParam("p1") String p1, + @FormParam("p2") Integer p2) { + } + + @POST + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public void multipart(@FormDataParam("p1") String p1, + @FormDataParam("file1") InputStream file1, + @FormDataParam("p2") FormDataBodyPart file2) { + } + + @POST + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public void multipart2(@FormDataParam("p1") String p1, + @FormDataParam("file") InputStream file, + @FormDataParam("file") FormDataContentDisposition fileDisposition) { + } + + @Path("/entity") + @POST + public void post2(ModelType model) { + } + + @Path("/entity2") + @POST + public void post3(@NotNull ModelType model) { + } + + public static class ModelType { + private String name; + + public ModelType() { + } + + public ModelType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/InterfaceResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/InterfaceResource.java new file mode 100644 index 000000000..50d7acf44 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/InterfaceResource.java @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; + +import java.io.InputStream; + +/** + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +public class InterfaceResource implements ResourceDeclaration { + + @Override + public Response get(String sm, + String q, + String hh, + String cc) { + return Response.ok().build(); + } + + @Override + public String nopath() { + return "nopath"; + } + + @Override + public Response get(MappedBean bean) { + return Response.ok().build(); + } + + @Override + public void form(String p1, + Integer p2) { + } + + @Override + public void multipart(String p1, + InputStream file1, + FormDataBodyPart file2) { + } + + @Override + public void multipart2(String p1, + InputStream file, + FormDataContentDisposition fileDisposition) { + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/MappedBean.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/MappedBean.java new file mode 100644 index 000000000..78552e949 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/MappedBean.java @@ -0,0 +1,64 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; + +/** + * @author Vyacheslav Rusakov + * @since 30.09.2025 + */ +public class MappedBean { + @PathParam("sm") + private String sm; + @QueryParam("q") + private String q; + @HeaderParam("HH") + private String hh; + @CookieParam("cc") + private String cc; + @MatrixParam("mm") + private String mm; + + public String getSm() { + return sm; + } + + public void setSm(String sm) { + this.sm = sm; + } + + public String getQ() { + return q; + } + + public void setQ(String q) { + this.q = q; + } + + public String getHh() { + return hh; + } + + public void setHh(String hh) { + this.hh = hh; + } + + public String getCc() { + return cc; + } + + public void setCc(String cc) { + this.cc = cc; + } + + public String getMm() { + return mm; + } + + public void setMm(String mm) { + this.mm = mm; + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/NoPathResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/NoPathResource.java new file mode 100644 index 000000000..d2c344501 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/NoPathResource.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 14.10.2025 + */ +public class NoPathResource { + + @GET + @Path("/{sm}/") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("sm") String sm, + @QueryParam("q") String q, + @HeaderParam("HH") String hh, + @CookieParam("cc") String cc, + @MatrixParam("mm") String mm) { + return Response.ok().build(); + } + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/ResourceDeclaration.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/ResourceDeclaration.java new file mode 100644 index 000000000..ce4ef08c4 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/ResourceDeclaration.java @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.InputStream; + +/** + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +@Path("/iface/") +public interface ResourceDeclaration { + + @GET + @Path("/{sm}/") + @Produces(MediaType.APPLICATION_JSON) + Response get(@PathParam("sm") String sm, + @QueryParam("q") String q, + @HeaderParam("HH") String hh, + @CookieParam("cc") String cc); + + @GET + String nopath(); + + @Path("/{sm}/2") + Response get(MappedBean bean); + + @Path("/form") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + void form(@FormParam("p1") String p1, + @FormParam("p2") Integer p2); + + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + void multipart(@FormDataParam("p1") String p1, + @FormDataParam("file1") InputStream file1, + @FormDataParam("file2") FormDataBodyPart file2); + + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + void multipart2(@FormDataParam("p1") String p1, + @FormDataParam("file") InputStream file, + @FormDataParam("file") FormDataContentDisposition fileDisposition); +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/SuperclassDeclarationResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/SuperclassDeclarationResource.java new file mode 100644 index 000000000..f3fa3461a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/SuperclassDeclarationResource.java @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.url.resource.support; + +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; + +import java.io.InputStream; + +/** + * @author Vyacheslav Rusakov + * @since 29.09.2025 + */ +public class SuperclassDeclarationResource extends DirectResource { + + @Override + public Response get(String sm, String q, String hh, String cc, String mm) { + return super.get(sm, q, hh, cc, mm); + } + + @Override + public Response get(MappedBean bean) { + return super.get(bean); + } + + @Override + public void form(String p1, Integer p2) { + super.form(p1, p2); + } + + @Override + public void multipart(String p1, InputStream file1, FormDataBodyPart file2) { + super.multipart(p1, file1, file2); + } + + @Override + public void multipart2(String p1, InputStream file, FormDataContentDisposition fileDisposition) { + super.multipart2(p1, file, fileDisposition); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/RootResource.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/RootResource.java new file mode 100644 index 000000000..a09b42c5c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/RootResource.java @@ -0,0 +1,16 @@ +package ru.vyarus.dropwizard.guice.url.resource.support.sub; + +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 02.10.2025 + */ +@Path("/root") +public class RootResource { + + @Path("/sub1") + public SubResource1 sub1() { + return new SubResource1(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource1.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource1.java new file mode 100644 index 000000000..5bfcac5bd --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource1.java @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.url.resource.support.sub; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; + +/** + * @author Vyacheslav Rusakov + * @since 02.10.2025 + */ +// intentionally no path annotation +public class SubResource1 { + + @Path("/sub2/{name}") + @Consumes(MediaType.TEXT_PLAIN) + public SubResource2 sub2(@PathParam("name") String name) { + return new SubResource2(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource2.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource2.java new file mode 100644 index 000000000..c12fa8b2a --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/resource/support/sub/SubResource2.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.url.resource.support.sub; + +import jakarta.ws.rs.CookieParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 02.10.2025 + */ +@Path("/sub2/") +public class SubResource2 { + + @GET + @Path("/{sm}/") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("sm") String sm) { + return Response.ok().build(); + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/util/RestPathUtilsTest.java b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/util/RestPathUtilsTest.java new file mode 100644 index 000000000..285ba3a3d --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/url/util/RestPathUtilsTest.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.url.util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.url.resource.support.DirectResource; + +/** + * @author Vyacheslav Rusakov + * @since 02.10.2025 + */ +public class RestPathUtilsTest { + + @Test + void testPathBuilding() { + + Assertions.assertThat(RestPathUtils.getResourcePath(DirectResource.class)) + .isEqualTo("/direct"); + Assertions.assertThat(RestPathUtils.getResourcePath("some/%s", DirectResource.class, 11)) + .isEqualTo("/some/11/direct"); + + Assertions.assertThat(RestPathUtils.buildPath(DirectResource.class) + .queryParam("q", 1).build()) + .isEqualTo("/direct?q=1"); + Assertions.assertThat(RestPathUtils.buildPath("some/%s", DirectResource.class, 11) + .queryParam("q", 1).build()) + .isEqualTo("/some/11/direct?q=1"); + + Assertions.assertThat(RestPathUtils.buildSubResourcePath("some/%s/", DirectResource.class, 11) + .queryParam("q", 1).build()) + .isEqualTo("/some/11?q=1"); + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy index fbbc7f798..ec36f311f 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/AutoScanWebTest.groovy @@ -3,10 +3,10 @@ package ru.vyarus.dropwizard.guice.web import com.google.inject.Binder import com.google.inject.Inject import com.google.inject.Injector -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy similarity index 97% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy index 5d4806404..00db10450 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextSimpleServerTest.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.web import com.google.inject.Inject -import io.dropwizard.setup.Environment +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.support.web.crosscontext.CrossContextListener import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy index 98b86c4a5..94ac9250c 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/CrossContextTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.web import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.web.crosscontext.CrossContextFilter diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy index 5bc716586..7ebab4160 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceFilterCustomDispatchersTest.groovy @@ -1,20 +1,20 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import org.eclipse.jetty.servlet.FilterMapping +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.eclipse.jetty.ee10.servlet.FilterMapping import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.jersey.GuiceWebModule import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject -import static javax.servlet.DispatcherType.ERROR -import static javax.servlet.DispatcherType.FORWARD -import static javax.servlet.DispatcherType.REQUEST +import static jakarta.servlet.DispatcherType.ERROR +import static jakarta.servlet.DispatcherType.FORWARD +import static jakarta.servlet.DispatcherType.REQUEST import static ru.vyarus.dropwizard.guice.GuiceyOptions.GuiceFilterRegistration /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy index 6c10255ed..d0239673e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/GuiceRequestScopePropagationTest.groovy @@ -1,19 +1,19 @@ package ru.vyarus.dropwizard.guice.web import com.google.inject.servlet.ServletScopes -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.inject.Provider -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.UriInfo +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.UriInfo import java.util.concurrent.CompletableFuture /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy index 2798f9b3c..9deb9f2af 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ListenersTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo @@ -11,7 +11,7 @@ import ru.vyarus.dropwizard.guice.module.installer.feature.web.listener.WebListe import ru.vyarus.dropwizard.guice.support.web.listeners.ContextListener import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy similarity index 81% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy index 851d2e389..07bff5954 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletGenNameCollisionTest.groovy @@ -1,17 +1,17 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import org.eclipse.jetty.servlet.ServletHolder +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.eclipse.jetty.ee10.servlet.ServletHolder import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet +import jakarta.inject.Inject +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy index b2aa4113f..45a1a93bc 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashFailTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.web.servletclash.Servlet1 @@ -21,7 +21,7 @@ class ServletMappingClashFailTest extends AbstractTest { def "Check servlets mapping clash"() { when: "starting app with servlets clash" - TestSupport.runWebApp(ClashApp, null) + TestSupport.runWebApp(ClashApp) then: "exception thrown" def ex = thrown(IllegalStateException) ex.message == "Servlet registration Servlet2 clash with already installed servlets on paths: /sam" diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy similarity index 85% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy index a805e29a8..498c63cef 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/ServletMappingClashTest.groovy @@ -1,9 +1,9 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.web.servletclash.Servlet1 diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy similarity index 75% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy index 46c8f6cae..bf5dfee49 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/SessionListenerTest.groovy @@ -1,12 +1,10 @@ package ru.vyarus.dropwizard.guice.web -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import org.eclipse.jetty.server.session.SessionHandler -import org.eclipse.jetty.util.component.AbstractLifeCycle -import org.eclipse.jetty.util.component.LifeCycle +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.eclipse.jetty.ee10.servlet.SessionHandler import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.web.session.SessionListener @@ -23,7 +21,7 @@ class SessionListenerTest extends AbstractTest { def "Check application startup without sessions"() { when: "starting app without session configured" - TestSupport.runWebApp(NSApp, null) + TestSupport.runWebApp(NSApp) then: "listeners were not installed - warning printed" true } @@ -34,13 +32,13 @@ class SessionListenerTest extends AbstractTest { TestSupport.runWebApp(NSFailApp, 'src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml') then: "error" def ex = thrown(IllegalStateException) - ex.message == 'Can\'t register session listeners for application context because sessions support is not enabled: SessionListener' + ex.message == 'Can\'t register session listeners for root because sessions support is not enabled: SessionListener' } def "Check session listener installation"() { when: "starting app with session configured" - TestSupport.runWebApp(SApp, null) + TestSupport.runWebApp(SApp) then: "listener installation ok" true } @@ -71,12 +69,6 @@ class SessionListenerTest extends AbstractTest { @Override void run(Configuration configuration, Environment environment) throws Exception { - environment.lifecycle().addLifeCycleListener(new LifeCycle.Listener() { - @Override - void lifeCycleStopping(LifeCycle event) { - (event as AbstractLifeCycle).stopTimeout = 0 - } - }) } } diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy index 21d9dc887..b51944097 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebItemsInitTest.groovy @@ -1,13 +1,13 @@ package ru.vyarus.dropwizard.guice.web import com.google.inject.Inject -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import org.eclipse.jetty.servlet.FilterHolder -import org.eclipse.jetty.servlet.FilterMapping -import org.eclipse.jetty.servlet.ServletHolder +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.eclipse.jetty.ee10.servlet.FilterHolder +import org.eclipse.jetty.ee10.servlet.FilterMapping +import org.eclipse.jetty.ee10.servlet.ServletHolder import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.support.web.params.InitParamsFilter @@ -15,7 +15,7 @@ import ru.vyarus.dropwizard.guice.support.web.params.InitParamsServlet import ru.vyarus.dropwizard.guice.support.web.params.ServletRegFilter import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.servlet.DispatcherType +import jakarta.servlet.DispatcherType /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebModuleTest.groovy similarity index 100% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/WebModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/WebModuleTest.groovy diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy index f116a0259..9ef502097 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncFilterTest.groovy @@ -1,16 +1,16 @@ package ru.vyarus.dropwizard.guice.web.async -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.servlet.* -import javax.servlet.annotation.WebFilter +import jakarta.servlet.* +import jakarta.servlet.annotation.WebFilter /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy index 5f5ca5a1e..2e2913cf5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncResourceTest.groovy @@ -1,18 +1,18 @@ package ru.vyarus.dropwizard.guice.web.async -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import org.glassfish.jersey.server.ManagedAsync import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.container.AsyncResponse -import javax.ws.rs.container.Suspended +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import java.util.concurrent.CompletableFuture /** diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy similarity index 80% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy index 64b2de2ae..0b1592d86 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/async/AsyncServletTest.groovy @@ -1,20 +1,20 @@ package ru.vyarus.dropwizard.guice.web.async -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp import spock.lang.Specification -import javax.servlet.AsyncContext -import javax.servlet.ServletException -import javax.servlet.annotation.WebServlet -import javax.servlet.http.HttpServlet -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.AsyncContext +import jakarta.servlet.ServletException +import jakarta.servlet.annotation.WebServlet +import jakarta.servlet.http.HttpServlet +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy similarity index 76% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy index cf7b7e418..7e88bdf82 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceDenyServletModuleTest.groovy @@ -2,10 +2,10 @@ package ru.vyarus.dropwizard.guice.web.noguice import com.google.inject.CreationException import com.google.inject.servlet.ServletModule -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.TestSupport import spock.lang.Specification @@ -19,10 +19,10 @@ class NoGuiceDenyServletModuleTest extends Specification { def "Check servlet module denied without guice filter"() { when: "start app with servlet module and no filter" - TestSupport.runWebApp(DenySMApp, null) + TestSupport.runWebApp(DenySMApp) then: "error" def ex = thrown(CreationException) - ex.errorMessages[0].message.equals("javax.servlet.http.HttpServletRequest was bound multiple times.") + ex.errorMessages[0].message.equals("jakarta.servlet.http.HttpServletRequest was bound multiple times.") } static class DenySMApp extends Application { diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy similarity index 77% rename from src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy index 60600e5ea..6f7b624f1 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/web/noguice/NoGuiceFilterTest.groovy @@ -1,20 +1,20 @@ package ru.vyarus.dropwizard.guice.web.noguice -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import javax.inject.Inject -import javax.inject.Provider -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.SecurityContext +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.SecurityContext /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy similarity index 94% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy index 652c05173..3a6ef8ff4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigAccessorsTest.groovy @@ -1,10 +1,10 @@ package ru.vyarus.dropwizard.guice.yaml import com.fasterxml.jackson.annotation.JsonProperty -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.yaml.ConfigPath import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder @@ -12,7 +12,7 @@ import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy index 54e4c8abb..0dae2c4bf 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsTest.groovy @@ -4,11 +4,11 @@ import com.google.inject.Binding import com.google.inject.Injector import com.google.inject.Key import com.google.inject.name.Names -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.server.ServerFactory -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.server.ServerFactory +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree @@ -16,7 +16,7 @@ import ru.vyarus.dropwizard.guice.module.yaml.bind.Config import ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigImpl import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy index a32c35e7d..856cc8491 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigBindingsWithDisabledIntrospectionTest.groovy @@ -3,12 +3,12 @@ package ru.vyarus.dropwizard.guice.yaml import com.google.inject.Binding import com.google.inject.Injector import com.google.inject.Key -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.logging.LoggingFactory -import io.dropwizard.server.ServerFactory -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.logging.common.LoggingFactory +import io.dropwizard.core.server.ServerFactory +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.GuiceyOptions @@ -17,7 +17,7 @@ import ru.vyarus.dropwizard.guice.module.yaml.bind.Config import ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigImpl import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy index b88e54ceb..f5f323d91 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ConfigInspectorTest.groovy @@ -1,21 +1,22 @@ package ru.vyarus.dropwizard.guice.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration +import com.google.inject.name.Named +import com.google.inject.name.Names +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration import io.dropwizard.jersey.filter.AllowedMethodsFilter import io.dropwizard.jetty.GzipHandlerFactory -import io.dropwizard.jetty.ServerPushFilterFactory -import io.dropwizard.logging.DefaultLoggingFactory -import io.dropwizard.logging.LoggingFactory -import io.dropwizard.metrics.MetricsFactory +import io.dropwizard.logging.common.DefaultLoggingFactory +import io.dropwizard.logging.common.LoggingFactory +import io.dropwizard.metrics.common.MetricsFactory import io.dropwizard.request.logging.RequestLogFactory -import io.dropwizard.server.DefaultServerFactory -import io.dropwizard.server.ServerFactory +import io.dropwizard.core.server.DefaultServerFactory +import io.dropwizard.core.server.ServerFactory import io.dropwizard.servlets.tasks.TaskConfiguration -import io.dropwizard.setup.AdminFactory -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import io.dropwizard.setup.HealthCheckConfiguration +import io.dropwizard.core.setup.AdminFactory +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.core.setup.HealthCheckConfiguration import io.dropwizard.util.Duration import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.yaml.ConfigPath @@ -25,7 +26,7 @@ import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import ru.vyarus.dropwizard.guice.yaml.support.* import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -44,7 +45,9 @@ class ConfigInspectorTest extends Specification { when: "check default config" def res = ConfigTreeBuilder.build(bootstrap, create(Configuration)) then: - printConfig(res) == """[Configuration] admin (AdminFactory) = AdminFactory[healthChecks=HealthCheckConfiguration[servletEnabled= true, minThreads=1, maxThreads=4, workQueueSize=1], tasks=TaskConfiguration[printStackTraceOnError=false]] + printConfig(res) + .replace('[HEAD, DELETE, POST, GET, OPTIONS, PATCH, PUT]', '[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]') + == """[Configuration] admin (AdminFactory) = AdminFactory[healthChecks=HealthCheckConfiguration[servletEnabled= true, minThreads=1, maxThreads=4, workQueueSize=1], tasks=TaskConfiguration[printStackTraceOnError=false]] [Configuration] admin.healthChecks (HealthCheckConfiguration) = HealthCheckConfiguration[servletEnabled= true, minThreads=1, maxThreads=4, workQueueSize=1] [Configuration] admin.healthChecks.maxThreads (Integer) = 4 [Configuration] admin.healthChecks.minThreads (Integer) = 1 @@ -53,8 +56,8 @@ class ConfigInspectorTest extends Specification { [Configuration] admin.tasks (TaskConfiguration) = TaskConfiguration[printStackTraceOnError=false] [Configuration] admin.tasks.printStackTraceOnError (Boolean) = false [Configuration] health (Optional) = Optional.empty -[Configuration] logging (LoggingFactory as DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.ConsoleAppenderFactory@1111111]} -[Configuration] logging.appenders (List> as ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] +[Configuration] logging (LoggingFactory as DefaultLoggingFactory) = DefaultLoggingFactory{level=INFO, loggers={}, appenders=[io.dropwizard.logging.common.ConsoleAppenderFactory@1111111]} +[Configuration] logging.appenders (List> as ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] [Configuration] logging.level (String) = "INFO" [Configuration] logging.loggers (Map as HashMap) = {} [Configuration] metrics (MetricsFactory) = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} @@ -72,7 +75,9 @@ class ConfigInspectorTest extends Specification { [Configuration] server.detailedJsonProcessingExceptionMapper (Boolean) = false [Configuration] server.dumpAfterStart (Boolean) = false [Configuration] server.dumpBeforeStop (Boolean) = false +[Configuration] server.enableAdminVirtualThreads (Boolean) = false [Configuration] server.enableThreadNameFilter (Boolean) = true +[Configuration] server.enableVirtualThreads (Boolean) = false [Configuration] server.gid (Integer) = null [Configuration] server.group (String) = null [Configuration] server.gzip (GzipHandlerFactory) = io.dropwizard.jetty.GzipHandlerFactory@1111111 @@ -82,36 +87,29 @@ class ConfigInspectorTest extends Specification { [Configuration] server.gzip.enabled (Boolean) = true [Configuration] server.gzip.excludedMimeTypes (Set) = null [Configuration] server.gzip.excludedPaths (Set) = null -[Configuration] server.gzip.excludedUserAgentPatterns (Set as HashSet) = [] -[Configuration] server.gzip.gzipCompatibleInflation (Boolean) = true [Configuration] server.gzip.includedMethods (Set) = null [Configuration] server.gzip.includedPaths (Set) = null [Configuration] server.gzip.minimumEntitySize (DataSize) = 256 bytes [Configuration] server.gzip.syncFlush (Boolean) = false [Configuration] server.idleThreadTimeout (Duration) = 1 minute -[Configuration] server.maxQueuedRequests (Integer) = 1024 [Configuration] server.maxThreads (Integer) = 1024 +[Configuration] server.metricPrefix (String) = null [Configuration] server.minThreads (Integer) = 8 [Configuration] server.nofileHardLimit (Integer) = null [Configuration] server.nofileSoftLimit (Integer) = null [Configuration] server.registerDefaultExceptionMappers (Boolean) = true [Configuration] server.requestLog (RequestLogFactory as LogbackAccessRequestLogFactory) = io.dropwizard.request.logging.LogbackAccessRequestLogFactory@1111111 -[Configuration] server.requestLog.appenders (List> as ArrayList>) = [io.dropwizard.logging.ConsoleAppenderFactory@1111111] +[Configuration] server.requestLog.appenders (List> as ArrayList>) = [io.dropwizard.logging.common.ConsoleAppenderFactory@1111111] +[Configuration] server.responseMeteredLevel (ResponseMeteredLevel) = COARSE [Configuration] server.rootPath (Optional) = Optional.empty -[Configuration] server.serverPush (ServerPushFilterFactory) = io.dropwizard.jetty.ServerPushFilterFactory@1111111 -[Configuration] server.serverPush.associatePeriod (Duration) = 4 seconds -[Configuration] server.serverPush.enabled (Boolean) = false -[Configuration] server.serverPush.maxAssociations (Integer) = 16 -[Configuration] server.serverPush.refererHosts (List) = null -[Configuration] server.serverPush.refererPorts (List) = null [Configuration] server.shutdownGracePeriod (Duration) = 30 seconds [Configuration] server.startsAsRoot (Boolean) = null [Configuration] server.uid (Integer) = null [Configuration] server.umask (String) = null [Configuration] server.user (String) = null""" res.rootTypes == [Configuration] - res.uniqueTypePaths.size() == 9 - res.paths.size() == 65 + res.uniqueTypePaths.size() == 8 + res.paths.size() == 60 check(res, "server", DefaultServerFactory) check(res, "server.maxThreads", Integer, 1024) check(res, "server.idleThreadTimeout", Duration, Duration.minutes(1)) @@ -126,8 +124,8 @@ class ConfigInspectorTest extends Specification { [SimpleConfig] foo (String) = null [SimpleConfig] prim (Integer) = 0""" res.rootTypes == [SimpleConfig, Configuration] - res.uniqueTypePaths.size() == 9 - res.paths.size() == 68 + res.uniqueTypePaths.size() == 8 + res.paths.size() == 63 check(res, "foo", String) check(res, "bar", Boolean) check(res, "prim", Integer) @@ -138,8 +136,8 @@ class ConfigInspectorTest extends Specification { then: "Object remain as declared type" printConfig(res) == "[ObjectPropertyConfig] sub (Object) = null" res.rootTypes == [ObjectPropertyConfig, Configuration] - res.uniqueTypePaths.size() == 9 - res.paths.size() == 66 + res.uniqueTypePaths.size() == 8 + res.paths.size() == 61 check(res, "sub", Object) elt.isObjectDeclaration() elt.declaredType == Object @@ -169,10 +167,10 @@ class ConfigInspectorTest extends Specification { [ComplexConfig] sub.two (ComplexConfig.Parametrized) = null [ComplexConfig] sub.two.list (List) = null""" res.rootTypes == [ComplexConfig, Iface, Configuration] - res.uniqueTypePaths.size() == 10 + res.uniqueTypePaths.size() == 9 res.uniqueTypePaths.find { it.valueType == ComplexConfig.SubConfig } != null res.uniqueTypePaths.find { it.valueType == ComplexConfig.Parametrized } == null - res.paths.size() == 71 + res.paths.size() == 66 check(res, "sub", ComplexConfig.SubConfig) check(res, "sub.sub", String) check(res, "sub.two", ComplexConfig.Parametrized, null, String) @@ -237,7 +235,7 @@ class ConfigInspectorTest extends Specification { def res = ConfigTreeBuilder.build(bootstrap, create(NotUniqueSubConfig)) then: res.getUniqueTypePaths().collect { it.getDeclaredType() } as Set == - [ServerPushFilterFactory, LoggingFactory, ServerFactory, GzipHandlerFactory, MetricsFactory, RequestLogFactory, + [LoggingFactory, ServerFactory, GzipHandlerFactory, MetricsFactory, RequestLogFactory, TaskConfiguration, AdminFactory, HealthCheckConfiguration] as Set res.findByPath("sub1").getDeclaredType() == NotUniqueSubConfig.SubConfig res.findByPath("sub1.sub").getDeclaredType() == String @@ -258,7 +256,7 @@ class ConfigInspectorTest extends Specification { res = ConfigTreeBuilder.build(bootstrap, config) then: res.getUniqueTypePaths().collect { it.getDeclaredType() } as Set == - [ComplexConfig.SubConfig, ServerPushFilterFactory, LoggingFactory, ServerFactory, GzipHandlerFactory, MetricsFactory, RequestLogFactory, + [ComplexConfig.SubConfig, LoggingFactory, ServerFactory, GzipHandlerFactory, MetricsFactory, RequestLogFactory, TaskConfiguration, AdminFactory, HealthCheckConfiguration] as Set res.findByPath("sub").getDeclaredType() == ComplexConfig.SubConfig res.findByPath("sub.sub").getDeclaredType() == String @@ -322,6 +320,41 @@ class ConfigInspectorTest extends Specification { res.valueByUniqueDeclaredType(ComplexGenericCase.Sub) instanceof ComplexGenericCase.Sub } + def "Check qualified fields support"() { + + when: "config with qualified values" + def config = create(AnnotatedConfig) + config.prop = "1" + config.prop2 = "2" + config.prop3 = 3 + config.custom = "cust" + def res = ConfigTreeBuilder.build(bootstrap, config) + then: + res.findAllByAnnotation(Named).collect {it.path} == ["prop", "prop2", "prop3"] + res.findAllByAnnotation(CustQualifier).collect {it.path} == ["custom"] + res.findAllByAnnotation(Names.named("test2")).collect {it.path} == ["prop2", "prop3"] + res.findByAnnotation(Names.named("test")).path == "prop" + res.findByAnnotation(CustQualifier).path == "custom" + + res.annotatedValues(Named) == ["1", "2", 3] as Set + res.annotatedValues(Names.named("test2")) == ["2", 3] as Set + res.annotatedValues(CustQualifier) == ["cust"] as Set + res.annotatedValue(Names.named("test")) == "1" + res.annotatedValue(CustQualifier) == "cust" + + when: "non-unique call" + res.annotatedValue(Named) + then: + def ex = thrown(IllegalStateException) + ex.message.startsWith("Multiple configuration paths qualified with annotation type @Named") + + when: "non-unique call2" + res.annotatedValue(Names.named("test2")) + then: + def ex2 = thrown(IllegalStateException) + ex2.message.startsWith("Multiple configuration paths qualified with annotation @Named(\"test2\")") + } + def "Check item methods"() { when: "1st level path" diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy index 925eb5331..13ec77759 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/NestedMapsConfigTest.groovy @@ -1,15 +1,15 @@ package ru.vyarus.dropwizard.guice.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.AbstractTest import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy similarity index 89% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy index 01e517376..08d794c30 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/RealBindingsTest.groovy @@ -1,15 +1,15 @@ package ru.vyarus.dropwizard.guice.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.yaml.bind.Config import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy similarity index 59% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy index ec69dbd63..ce71d8351 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/ShortcutsTest.groovy @@ -1,20 +1,24 @@ package ru.vyarus.dropwizard.guice.yaml -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import com.google.inject.name.Named +import com.google.inject.name.Names +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule import ru.vyarus.dropwizard.guice.module.yaml.ConfigTreeBuilder import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.yaml.support.AnnotatedConfig import ru.vyarus.dropwizard.guice.yaml.support.ComplexGenericCase +import ru.vyarus.dropwizard.guice.yaml.support.CustQualifier import ru.vyarus.dropwizard.guice.yaml.support.NotUniqueSubConfig import spock.lang.Specification -import javax.inject.Inject +import jakarta.inject.Inject /** * @author Vyacheslav Rusakov @@ -55,6 +59,33 @@ class ShortcutsTest extends Specification { mod.configuration(ComplexGenericCase.Sub) != null mod.configurations(ComplexGenericCase.Sub).size() == 1 + + when: "config with qualified properties" + config = create(AnnotatedConfig) + config.prop = "1" + config.prop2 = "2" + config.prop3 = 3 + config.custom = "cust" + res = ConfigTreeBuilder.build(bootstrap, config) + mod = new DropwizardAwareModule() {} + mod.setConfigurationTree(res) + then: + mod.annotatedConfiguration(CustQualifier) == "cust" + mod.configurationTree().annotatedValues(CustQualifier) == ["cust"] as Set + mod.annotatedConfiguration(Names.named("test")) == "1" + mod.configurationTree().annotatedValues(Names.named("test2")) == ["2", 3] as Set + + when: "non-unique call" + mod.annotatedConfiguration(Named) + then: + def ex = thrown(IllegalStateException) + ex.message.startsWith("Multiple configuration paths qualified with annotation type @Named") + + when: "non-unique call2" + mod.annotatedConfiguration(Names.named("test2")) + then: + def ex2 = thrown(IllegalStateException) + ex2.message.startsWith("Multiple configuration paths qualified with annotation @Named(\"test2\")") } def "Check bundle shortcuts"() { @@ -88,6 +119,34 @@ class ShortcutsTest extends Specification { bundle.configuration(ComplexGenericCase.Sub) != null bundle.configurations(ComplexGenericCase.Sub).size() == 1 + + when: "config with qualified properties" + config = create(AnnotatedConfig) + config.prop = "1" + config.prop2 = "2" + config.prop3 = 3 + config.custom = "cust" + res = ConfigTreeBuilder.build(bootstrap, config) + context = new ConfigurationContext() + context.configurationTree = res + bundle = new GuiceyEnvironment(context) + then: + bundle.annotatedConfiguration(CustQualifier) == "cust" + bundle.configurationTree().annotatedValues(CustQualifier) == ["cust"] as Set + bundle.annotatedConfiguration(Names.named("test")) == "1" + bundle.configurationTree().annotatedValues(Names.named("test2")) == ["2", 3] as Set + + when: "non-unique call" + bundle.annotatedConfiguration(Named) + then: + def ex = thrown(IllegalStateException) + ex.message.startsWith("Multiple configuration paths qualified with annotation type @Named") + + when: "non-unique call2" + bundle.annotatedConfiguration(Names.named("test2")) + then: + def ex2 = thrown(IllegalStateException) + ex2.message.startsWith("Multiple configuration paths qualified with annotation @Named(\"test2\")") } private T create(Class type) { diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/CoreConfigObjectsQualificationBindingTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/CoreConfigObjectsQualificationBindingTest.groovy new file mode 100644 index 000000000..76481662c --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/CoreConfigObjectsQualificationBindingTest.groovy @@ -0,0 +1,96 @@ +package ru.vyarus.dropwizard.guice.yaml.qualifier + +import com.google.inject.Inject +import com.google.inject.name.Named +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.metrics.common.MetricsFactory +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig +import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer +import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 13.11.2023 + */ +@TestGuiceyApp(App) +class CoreConfigObjectsQualificationBindingTest extends Specification { + + @Inject + @Named("metrics") + MetricsFactory metrics + + @Inject + Config config + + @Inject + ConfigurationTree tree + + def 'Check qualification bindings'() { + + expect: "qualified bindings recognized" + metrics == config.getMetricsFactory() + + and: "report correct" + render(new BindingsConfig() + .showCustomConfigOnly()) == """ + + Configuration object bindings: + @Config Config + + + Unique sub configuration objects bindings: + + Config.metrics + @Config MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} + + + Qualified bindings: + @Named("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} (metrics) + + + Configuration paths bindings: + + Config: + @Config("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} + @Config("metrics.frequency") Duration = 1 minute + @Config("metrics.reportOnStop") Boolean = false + @Config("metrics.reporters") List (with actual type ArrayList) = [] +""" + } + + String render(BindingsConfig config) { + new ConfigBindingsRenderer(tree).renderReport(config) + .replaceAll("\r", "") + .replaceAll(" +\n", "\n") + .replaceAll('@(\\d+|[a-z])[^]C \n]+', '@1111111') + } + + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printCustomConfigurationBindings() + .build()) + } + + @Override + void run(Config config, Environment environment) throws Exception { + } + } + + static class Config extends Configuration { + + @Named("metrics") + @Override + MetricsFactory getMetricsFactory() { + return super.getMetricsFactory() + } + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedAggregationTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedAggregationTest.groovy new file mode 100644 index 000000000..642927275 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedAggregationTest.groovy @@ -0,0 +1,99 @@ +package ru.vyarus.dropwizard.guice.yaml.qualifier + +import com.google.inject.Inject +import com.google.inject.name.Named +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig +import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer +import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 13.11.2023 + */ +@TestGuiceyApp(value = App, configOverride = ["one1.val:1", "one2.val:2", "two.val:3"]) +class QualifiedAggregationTest extends Specification { + + @Inject + @Named("one") + Set subs + + @Inject + @Named("two") + Sub two + + @Inject + ConfigurationTree tree + + def "Check grouped bindings"() { + + expect: + subs.size() == 2 + two.val == 3 + and: "report correct" + render(new BindingsConfig() + .showCustomConfigOnly()) == """ + + Configuration object bindings: + @Config Config + + + Qualified bindings: + @Named("one") Set = (aggregated values) + Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 (one1) + Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 (one2) + @Named("two") Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 (two) + + + Configuration paths bindings: + + Config: + @Config("one1") Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 + @Config("one1.val") Integer = 1 + @Config("one2") Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 + @Config("one2.val") Integer = 2 + @Config("two") Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiedAggregationTest\$Sub@1111111 + @Config("two.val") Integer = 3 +""" + } + + String render(BindingsConfig config) { + new ConfigBindingsRenderer(tree).renderReport(config) + .replaceAll("\r", "") + .replaceAll(" +\n", "\n") + .replaceAll('@(\\d+|[a-z])[^]C \n]+', '@1111111') + } + + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printCustomConfigurationBindings() + .build()) + } + + @Override + void run(Config config, Environment environment) throws Exception { + } + } + + static class Config extends Configuration { + @Named("one") + Sub one1 = new Sub() + @Named("one") + Sub one2 = new Sub() + @Named("two") + Sub two = new Sub() + } + + static class Sub { + Integer val + } +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedSampleNullValuesTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedSampleNullValuesTest.groovy new file mode 100644 index 000000000..a2dd9f129 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiedSampleNullValuesTest.groovy @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.yaml.qualifier + +import com.google.inject.Inject +import com.google.inject.name.Named +import io.dropwizard.metrics.common.MetricsFactory +import jakarta.annotation.Nullable +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 28.11.2023 + */ +// see QualifierSampleTest +@TestGuiceyApp(QualifierSampleTest.App) +class QualifiedSampleNullValuesTest extends Specification { + + @Inject @Nullable @Named("custom") String prop1 + @Inject @CustomQualifier QualifierSampleTest.SubObj obj1 + @Inject @Named("sub-prop") Set prop23 + @Inject @Named("metrics") MetricsFactory metricsFactyry + + // both annotations work + @Inject @Nullable @jakarta.inject.Named("ee") String ee + @Inject @Nullable @Named("ee") String ee2 + + def "Check qualified bindings"() { + + expect: + prop1 == null + obj1 != null + obj1.prop2 == null + ee == null + ee2 == null + prop23 == [null] as Set + metricsFactyry != null + } + + +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifierSampleTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifierSampleTest.groovy new file mode 100644 index 000000000..dd9b01b17 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifierSampleTest.groovy @@ -0,0 +1,111 @@ +package ru.vyarus.dropwizard.guice.yaml.qualifier + +import com.google.inject.BindingAnnotation +import com.google.inject.Inject +import com.google.inject.name.Named +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.metrics.common.MetricsFactory +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * @author Vyacheslav Rusakov + * @since 22.11.2023 + */ +// see also QualifiedSampleNullValuesTest +@TestGuiceyApp(value = App, configOverride = ["prop1:1", "ee:11", "obj1.prop2:2", "obj1.prop3:3"]) +class QualifierSampleTest extends Specification { + + @Inject @Named("custom") String prop1 + @Inject @CustomQualifier SubObj obj1 + @Inject @Named("sub-prop") Set prop23 + @Inject @Named("metrics") MetricsFactory metricsFactyry + + // both annotations work + @Inject @jakarta.inject.Named("ee") String ee + @Inject @Named("ee") String ee2 + + def "Check qualified bindings"() { + + expect: + prop1 == "1" + obj1 != null + obj1.prop2 == "2" + ee != null + ee2 != null + prop23 == ["2", "3"] as Set + metricsFactyry != null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printCustomConfigurationBindings() + .build()) + } + + @Override + void run(MyConfig configuration, Environment environment) throws Exception { + + } + } + + static class MyConfig extends Configuration { + @Named("custom") + private String prop1 + @CustomQualifier + private SubObj obj1 = new SubObj() + @jakarta.inject.Named("ee") + private String ee + + String getProp1() { + return prop1 + } + + SubObj getObj1() { + return obj1 + } + + String getEe() { + return ee + } + + @Named("metrics") // dropwizard object bind + @Override + MetricsFactory getMetricsFactory() { + return super.getMetricsFactory() + } + } + + static class SubObj { + private String prop2 + private String prop3 + + // aggregated binding (same type + qualifier) + @Named("sub-prop") + String getProp2() { + return prop2 + } + + @Named("sub-prop") + String getProp3() { + return prop3 + } + } +} + +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD]) +@BindingAnnotation +public @interface CustomQualifier {} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiersTest.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiersTest.groovy new file mode 100644 index 000000000..4e45684d3 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/qualifier/QualifiersTest.groovy @@ -0,0 +1,141 @@ +package ru.vyarus.dropwizard.guice.yaml.qualifier + +import com.google.inject.BindingAnnotation +import com.google.inject.Inject +import com.google.inject.name.Named +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.debug.report.yaml.BindingsConfig +import ru.vyarus.dropwizard.guice.debug.report.yaml.ConfigBindingsRenderer +import ru.vyarus.dropwizard.guice.module.yaml.ConfigurationTree +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * @author Vyacheslav Rusakov + * @since 11.11.2023 + */ +@TestGuiceyApp(value = App, configOverride = ["one:1", "sub.two:2", "three:3", "box.foo:4"]) +class QualifiersTest extends Specification { + + @Named("one") + @Inject + String one + + @Qualif + @Inject + Integer two + + @Qualif + @Inject + Box box + + @Named("custom") + @Inject + String custom + + @Inject + ConfigurationTree tree + + def 'Check qualification bindings'() { + + expect: "qualified bindings recognized" + one == "1" + two == 2 + box.foo == 4 + custom == "3" + + and: "report correct" + render(new BindingsConfig() + .showCustomConfigOnly()) == """ + + Configuration object bindings: + @Config Config + + + Unique sub configuration objects bindings: + + Config.box + @Config Box = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiersTest\$Box@1111111 + + Config.sub + @Config Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiersTest\$Sub@1111111 + + + Qualified bindings: + @Qualif Box = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiersTest\$Box@1111111 (box) + @Named("one") String = "1" (one) + @Qualif Integer = 2 (sub.two) + @Named("custom") String = "3" (three) + + + Configuration paths bindings: + + Config: + @Config("box") Box = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiersTest\$Box@1111111 + @Config("box.foo") Integer = 4 + @Config("one") String = "1" + @Config("sub") Sub = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifiersTest\$Sub@1111111 + @Config("sub.two") Integer = 2 + @Config("three") String = "3" +""" + } + + String render(BindingsConfig config) { + new ConfigBindingsRenderer(tree).renderReport(config) + .replaceAll("\r", "") + .replaceAll(" +\n", "\n") + .replaceAll('@(\\d+|[a-z])[^]C \n]+', '@1111111') + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .printCustomConfigurationBindings() + .build()) + } + + @Override + void run(Config config, Environment environment) throws Exception { + } + } + + static class Config extends Configuration { + @Named("one") + String one + Sub sub = new Sub() + @Qualif Box box = new Box() + + String three + + @Named("custom") + String getThree() { + return three + } + } + + static class Sub { + @Qualif + Integer two + } + + static class Box { + Integer foo + } + +} + +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD]) +@BindingAnnotation +public @interface Qualif { +} diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/AnnotatedConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/AnnotatedConfig.groovy new file mode 100644 index 000000000..a65d784a2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/AnnotatedConfig.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.yaml.support + +import com.google.inject.name.Named +import io.dropwizard.core.Configuration + +/** + * @author Vyacheslav Rusakov + * @since 23.11.2023 + */ +class AnnotatedConfig extends Configuration { + + @Named("test") + String prop + + @Named("test2") + String prop2 + + @Named("test2") + Integer prop3 + + @CustQualifier + String custom + +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy index 3b5bfa9c8..e21b176ca 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexConfig.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.yaml.support import com.fasterxml.jackson.annotation.JsonProperty -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy similarity index 92% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy index 37b819e2a..78f992ea4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ComplexGenericCase.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.yaml.support import com.fasterxml.jackson.annotation.JsonProperty -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/CustQualifier.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/CustQualifier.groovy new file mode 100644 index 000000000..2ef70fca2 --- /dev/null +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/CustQualifier.groovy @@ -0,0 +1,19 @@ +package ru.vyarus.dropwizard.guice.yaml.support + +import com.google.inject.BindingAnnotation + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * @author Vyacheslav Rusakov + * @since 23.11.2023 + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD]) +@BindingAnnotation +@interface CustQualifier { +} \ No newline at end of file diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy index 14481cf5a..1621c013e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/FailedGetterConfig.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy index 5e4384741..3747e051a 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/IgnorePathConfig.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.yaml.support import com.fasterxml.jackson.annotation.JsonIgnore -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy index d234df3a0..ab7589025 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/NotUniqueSubConfig.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy similarity index 86% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy index ce513bb1d..57b86084e 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/ObjectPropertyConfig.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.yaml.support import com.fasterxml.jackson.annotation.JsonProperty -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy index 9db3d8c72..47d9966a4 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveConfig.groovy @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy similarity index 88% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy index a93d95d76..1f6858087 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/RecursiveIndirectlyConfig.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy similarity index 83% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy index 9ca41dcfe..ebe0dd7c5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/SimpleConfig.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy similarity index 87% rename from src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy rename to dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy index 86f588ac4..0cfeddde6 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy +++ b/dropwizard-guicey/src/test/groovy/ru/vyarus/dropwizard/guice/yaml/support/TooBroadDeclarationConfig.groovy @@ -1,6 +1,6 @@ package ru.vyarus.dropwizard.guice.yaml.support -import io.dropwizard.Configuration +import io.dropwizard.core.Configuration /** * @author Vyacheslav Rusakov diff --git a/src/test/resources/logback.xml b/dropwizard-guicey/src/test/resources/logback.xml similarity index 100% rename from src/test/resources/logback.xml rename to dropwizard-guicey/src/test/resources/logback.xml diff --git a/src/test/resources/ru/vyarus/dropwizard/guice/admin/simpleServerConfig.yml b/dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/admin/simpleServerConfig.yml similarity index 100% rename from src/test/resources/ru/vyarus/dropwizard/guice/admin/simpleServerConfig.yml rename to dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/admin/simpleServerConfig.yml diff --git a/src/test/resources/ru/vyarus/dropwizard/guice/config.yml b/dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/config.yml similarity index 100% rename from src/test/resources/ru/vyarus/dropwizard/guice/config.yml rename to dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/config.yml diff --git a/src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml b/dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml similarity index 100% rename from src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml rename to dropwizard-guicey/src/test/resources/ru/vyarus/dropwizard/guice/simple-server.yml diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..f67dfc4c6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,43 @@ +# dropwizard-guicey examples +[![Examples CI](https://github.com/xvik/dropwizard-guicey/actions/workflows/examples-CI.yml/badge.svg)](https://github.com/xvik/dropwizard-guicey/actions/workflows/examples-CI.yml) + +### About + +NOTE: Older examples (before guicey 6, dropwizard 3) were published into the separate repository: + [dropwizard 2.1 examples](https://github.com/xvik/dropwizard-guicey-examples/tree/dw-2.1) + + +If you can't find answer for your problem in provided examples, please request new sample by +[creating new issue](https://github.com/xvik/dropwizard-guicey/issues). + + + +### Guicey core examples + +* [Getting started](core-getting-started) - example application from getting started documentation chapter +* [Extensions](core-extensions) - ways of extensions declaration +* [Servlets and filters](core-servlets) - servlets and filters registration example +* [Sub resources](core-rest-sub-resource) - sub resource usage example +* [Plug-n-play bundle](core-bundle-plug-n-play) - example of bundle, activated after its appearance in classpath +* [Default installers re-configuration](core-installers-reset) - using only subset of default installers +* [Custom installer implementation](core-installer-custom) - manual extension declaration example + +### Guicey ext modules examples + +* [JDBI3](ext-jdbi3) - JDBI3 ext module example +* [EventBus](ext-eventbus) - guava eventbus ext module example +* [SPA HTML5 routes](ext-spa) - SPA ext module example: HTML5 routes correct handling on server (for single page application) +* [Server pages exmaple](ext-gsp) - GSP example: server side templates and assets management +* [Server pages SPA exmaple](ext-gsp-spa) - use GSP templates for SPA index page + +### Other integrations + +* [Auth](integration-auth) - dropwizard-auth integration example +* [Hibernate](integration-hibernate) - dropwizard-hibernate integration example +* [Guice-validator](integration-guice-validator) - guice-validator integration example +* [Dropwizard-jobs](integration-dropwizard-jobs) - dropwizard-jobs integration example + +### Maven samples + +* [Simple](maven-simple) - dropwizard-guicey declared with direct dependency +* [BOM](maven-bom) - dropwizard-guicey declared with guicey BOM \ No newline at end of file diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 000000000..af3fe9039 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'com.github.ben-manes.versions' version '0.51.0' + id 'com.github.dkorotych.gradle-maven-exec' version '4.0.0' apply false + id 'org.openapi.generator' version '7.12.0' apply false +} + +wrapper { + gradleVersion = '8.10.1' +} + +description = 'Dropwizard guicey examples' + +ext { + guiceyBom = findProperty('guiceyBom') ?: '8.0.0' + dwVersion = '5.0.0' +} + +subprojects { + apply plugin: 'groovy' + apply plugin: 'project-report' + + java { + sourceCompatibility = 17 + } + + configurations.all { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + } + + repositories { + mavenLocal() + mavenCentral() + maven { + name = 'Central Portal Snapshots' + url = 'https://central.sonatype.com/repository/maven-snapshots/' + mavenContent { + snapshotsOnly() + includeGroupAndSubgroups('ru.vyarus') + } + } + } + dependencies { + implementation platform("ru.vyarus.guicey:guicey-bom:$guiceyBom") + + constraints { + implementation 'io.github.dropwizard-jobs:dropwizard-jobs-core:6.0.1' + + implementation 'org.flywaydb:flyway-core:10.18.0' + // flyway not compatible with h2 1.4.200 anymore + implementation 'com.h2database:h2:2.3.232' + } + + implementation 'ru.vyarus:dropwizard-guicey' + + testImplementation 'ru.vyarus:spock-junit5:1.2.0' + testImplementation 'org.spockframework:spock-core:2.4-M5-groovy-4.0' + testImplementation "io.dropwizard:dropwizard-testing" + testImplementation 'org.junit.jupiter:junit-jupiter-api' + + // required for junit 5.12 in current gradle version + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + test { + useJUnitPlatform() + testLogging { + events "skipped", "failed" + exceptionFormat "full" + } + maxHeapSize = "512m" + } + +} + +dependencyUpdates.revision = 'release' diff --git a/examples/core-bundle-plug-n-play/README.md b/examples/core-bundle-plug-n-play/README.md new file mode 100644 index 000000000..171ca7653 --- /dev/null +++ b/examples/core-bundle-plug-n-play/README.md @@ -0,0 +1,80 @@ +### Plug-n-play bundle + +Guicey allows [automatic bundles loading](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/bundles/#service-loader-lookup), +this way you can do bundles registered automatically as soon as bundle jar appear in classpath. + +Example: + +* [lifecycle annotations module](http://xvik.github.io/dropwizard-guicey/5.0.0/extras/lifecycle-annotations/) + + +In most cases, it makes sense to do such bundles with most commonly used configuration. +But user should be able to re-configure bundle if required. + +That's why it's better to mark bundle as unique (so only one instance of bundle will be accepted): + +```java +public class SampleBundle extends UniqueGuiceyBundle { + ... +} +``` + +It is not necessary to extend `UniqueGuiceyBundle`, +you can [implement equals and hash code yourself](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/bundles/#de-duplication) + + +[Bundles lookup](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/bundles/#bundle-lookup) appear after manually registered bundles +registration and so manually registered unique bundle will "override" default. + +To activate automatic registration we just need to add service descriptor: + +``` +META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +``` + +With bundle class inside: + +``` +ru.vyarus.dropwizard.guice.examples.bundle.SampleBundle +``` + +Now, sample application will indicate loaded bundle: + +````java +bootstrap.addBundle(GuiceBundle.builder() + // note: bundle not declared! + .printDiagnosticInfo() + .build()); +```` + +``` + BUNDLES = + CoreInstallersBundle (r.v.d.g.m.installer) + WebInstallersBundle (r.v.d.g.m.installer) + SampleBundle (r.v.d.g.e.bundle) *LOOKUP +``` + + +To override default bundle configuration, it must be registered manually: + +```java +bootstrap.addBundle(GuiceBundle.builder() + // override default bundle + .bundles(new SampleBundle("changed!")) + .build()); +``` + +NOTE: gucie constant binding used inside to check confguration correctness. + +``` + APPLICATION + │ + ├── SampleBundle (r.v.d.g.e.bundle) + │ └── module SampleBundle$1 (r.v.d.g.e.bundle) + │ + └── BUNDLES LOOKUP + └── -SampleBundle (r.v.d.g.e.bundle) *DUPLICATE +``` + +Here you can see that bundle from lookup was considered as duplicate, so only user-registered +bundle used. diff --git a/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleApplication.java b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleApplication.java new file mode 100644 index 000000000..896ee8a06 --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleApplication.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * Guicey plug-n-play bundle activation sample. + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +public class PlugnPlayBundleApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // note: bundle not declared! + .printDiagnosticInfo() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleOverrideApplication.java b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleOverrideApplication.java new file mode 100644 index 000000000..bb7bf7408 --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/PlugnPlayBundleOverrideApplication.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.bundle.SampleBundle; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +public class PlugnPlayBundleOverrideApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // override default bundle + .bundles(new SampleBundle("changed!")) + .printDiagnosticInfo() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/SampleBundle.java b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/SampleBundle.java new file mode 100644 index 000000000..0e8fd9b11 --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/SampleBundle.java @@ -0,0 +1,38 @@ +package ru.vyarus.dropwizard.guice.examples.bundle; + +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; + +/** + * To be able to override default bundle registration, bundle must be unique. In this case user will be able + * to re-configure it by simply applying bundle directly. + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +public class SampleBundle extends UniqueGuiceyBundle { + + private String config; + + public SampleBundle() { + this("default"); + } + + public SampleBundle(String config) { + this.config = config; + } + + @Override + public void initialize(GuiceyBootstrap bootstrap) { + bootstrap + .modules(new AbstractModule() { + @Override + protected void configure() { + // using constant binding to bypass bundle value + bind(String.class).annotatedWith(Names.named("bundle.config")).toInstance(config); + } + }); + } +} diff --git a/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/service/SampleService.java b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/service/SampleService.java new file mode 100644 index 000000000..36910188a --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/main/java/ru/vyarus/dropwizard/guice/examples/bundle/service/SampleService.java @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.examples.bundle.service; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +public class SampleService { + + @Inject @Named("bundle.config") + private String config; + + public String getConfig() { + return config; + } +} diff --git a/examples/core-bundle-plug-n-play/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle b/examples/core-bundle-plug-n-play/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle new file mode 100644 index 000000000..656d0b75a --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle @@ -0,0 +1 @@ +ru.vyarus.dropwizard.guice.examples.bundle.SampleBundle \ No newline at end of file diff --git a/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy b/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy new file mode 100644 index 000000000..f37f889a1 --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.bundle.SampleBundle +import ru.vyarus.dropwizard.guice.examples.bundle.service.SampleService +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +@TestGuiceyApp(PlugnPlayBundleApplication) +class AppTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + @Inject + SampleService service + + def "Check bundle installed"() { + + expect: "default config installed" + info.getBundlesFromLookup() as Set == [SampleBundle.class] as Set + service.getConfig() == "default" + + } +} \ No newline at end of file diff --git a/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/BundleOverrideTest.groovy b/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/BundleOverrideTest.groovy new file mode 100644 index 000000000..c2dc0cdb8 --- /dev/null +++ b/examples/core-bundle-plug-n-play/src/test/groovy/ru/vyarus/dropwizard/guice/examples/BundleOverrideTest.groovy @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.bundle.SampleBundle +import ru.vyarus.dropwizard.guice.examples.bundle.service.SampleService +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.module.context.ConfigScope +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@TestGuiceyApp(PlugnPlayBundleOverrideApplication) +class BundleOverrideTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + @Inject + SampleService service + + def "Check bundle installed"() { + + expect: "bundle overridden config installed" + info.getInfos(SampleBundle).size() == 1 + with(info.getInfo(SampleBundle)) { + getRegistrationAttempts() == 2 + getRegistrationScopeType() == ConfigScope.Application + } + service.getConfig() == "changed!" + + } +} diff --git a/examples/core-extensions/README.md b/examples/core-extensions/README.md new file mode 100644 index 000000000..11a3b2f13 --- /dev/null +++ b/examples/core-extensions/README.md @@ -0,0 +1,135 @@ +### Extensions registration example + +[Extensions](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/extensions/) could be registered: + +Directly: + +```java +GuiceBundle.builder() + .extensions(MyResource.class) + .build() +``` + +By classpath scan: + +```java +GuiceBundle.builder() + .enableAutoConfig("ru.vyarus.dropwizard.guice.examples.rest.scan") + .build() +``` + +(assume another resource is in specified package `ru.vyarus.dropwizard.guice.examples.rest.MyResource2`) + +Recognized from guice binding: + +```java +GuiceBundle.builder() + .modules(new AbstractModule() { + @Override + protected void configure() { + bind(MyResource3.class); + } + }) + .build() +``` + +Also, enabling diagnostic report to see what extensiosn would be found: + +```java +GuiceBundle.builder() + .pirntDiagnosticInfo() +``` + +When `ExtensionsDemoApplication` started, it will show: + +``` + INSTALLERS and EXTENSIONS in processing order = + resource (r.v.d.g.m.i.f.j.ResourceInstaller) + MyResource (r.v.d.g.e.rest) + MyResourceFromScan (r.v.d.g.e.rest.scan) *SCAN + MyResourceFromBinding (r.v.d.g.e.rest) *BINDING + + + APPLICATION + ├── extension MyResource (r.v.d.g.e.rest) + ├── module AppModule (r.v.d.guice.examples) + ├── module GuiceBootstrapModule (r.v.d.guice.module) + │ + ├── CoreInstallersBundle (r.v.d.g.m.installer) + │ ├── installer ResourceInstaller (r.v.d.g.m.i.f.jersey) + │ └── WebInstallersBundle (r.v.d.g.m.installer) + │ + ├── CLASSPATH SCAN + │ └── extension MyResourceFromScan (r.v.d.g.e.rest.scan) + │ + └── GUICE BINDINGS + │ + └── AppModule (r.v.d.guice.examples) + └── extension MyResourceFromBinding (r.v.d.g.e.rest) +``` + +As you can see, all extensions were recognized. + +Also, see sample spock tests using both [GuiceyAppRule](https://github.com/xvik/dropwizard-guicey#testing) (start only guice context - very fast) and +[DropwizardAppRule](http://www.dropwizard.io/1.0.0/docs/manual/testing.html) (when http server started). + +### Registration clash + +Another application shows what if extension declarations will clash: + +```java + bootstrap.addBundle(GuiceBundle.builder() + // scan will find everything + .enableAutoConfig("ru.vyarus.dropwizard.guice.examples") + // all three directly registered + .extensions(MyResource.class, MyResourceFromScan.class, MyResourceFromBinding.class) + // and all three set as bindings + .modules(new AppModule(), new AbstractModule() { + @Override + protected void configure() { + bind(MyResource.class); + bind(MyResourceFromScan.class); + } + }) + + // to show configured extensions + .printDiagnosticInfo() + .build()); +``` + +``` + INSTALLERS and EXTENSIONS in processing order = + resource (r.v.d.g.m.i.f.j.ResourceInstaller) + MyResource (r.v.d.g.e.rest) *SCAN, REG(1/3), BINDING + MyResourceFromScan (r.v.d.g.e.rest.scan) *SCAN, REG(1/3), BINDING + MyResourceFromBinding (r.v.d.g.e.rest) *SCAN, REG(1/3), BINDING + + APPLICATION + ├── extension MyResource (r.v.d.g.e.rest) + ├── extension MyResourceFromScan (r.v.d.g.e.rest.scan) + ├── extension MyResourceFromBinding (r.v.d.g.e.rest) + ├── module AppModule (r.v.d.guice.examples) + ├── module ExtensionsClashApplication$1 (r.v.d.guice.examples) + ├── module GuiceBootstrapModule (r.v.d.guice.module) + │ + ├── CoreInstallersBundle (r.v.d.g.m.installer) + │ ├── installer ResourceInstaller (r.v.d.g.m.i.f.jersey) + │ └── WebInstallersBundle (r.v.d.g.m.installer) + │ + ├── CLASSPATH SCAN + │ ├── extension -MyResource (r.v.d.g.e.rest) *DUPLICATE + │ ├── extension -MyResourceFromScan (r.v.d.g.e.rest.scan) *DUPLICATE + │ └── extension -MyResourceFromBinding (r.v.d.g.e.rest) *DUPLICATE + │ + └── GUICE BINDINGS + │ + ├── AppModule (r.v.d.guice.examples) + │ └── extension -MyResourceFromBinding (r.v.d.g.e.rest) *DUPLICATE + │ + └── ExtensionsClashApplication$1 (r.v.d.guice.examples) + ├── extension -MyResource (r.v.d.g.e.rest) *DUPLICATE + └── extension -MyResourceFromScan (r.v.d.g.e.rest.scan) *DUPLICATE + +``` + +Everything is ok: duplicate registrations simply ignored. \ No newline at end of file diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/AppModule.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/AppModule.java new file mode 100644 index 000000000..105e111d1 --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/AppModule.java @@ -0,0 +1,21 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.google.inject.AbstractModule; +import ru.vyarus.dropwizard.guice.examples.rest.MyResourceFromBinding; +import ru.vyarus.dropwizard.guice.examples.service.SampleService; +import ru.vyarus.dropwizard.guice.examples.service.SampleServiceImpl; + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +public class AppModule extends AbstractModule { + + @Override + protected void configure() { + bind(SampleService.class).to(SampleServiceImpl.class); + + // extension recognition from guice binding + bind(MyResourceFromBinding.class); + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsClashApplication.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsClashApplication.java new file mode 100644 index 000000000..93907598c --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsClashApplication.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.google.inject.AbstractModule; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.rest.MyResource; +import ru.vyarus.dropwizard.guice.examples.rest.MyResourceFromBinding; +import ru.vyarus.dropwizard.guice.examples.rest.scan.MyResourceFromScan; + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +public class ExtensionsClashApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // scan will find everything + .enableAutoConfig() + // all three directly registered + .extensions(MyResource.class, MyResourceFromScan.class, MyResourceFromBinding.class) + // and all three set as bindings + .modules(new AppModule(), new AbstractModule() { + @Override + protected void configure() { + bind(MyResource.class); + bind(MyResourceFromScan.class); + } + }) + + // to show configured extensions + .printDiagnosticInfo() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsDemoApplication.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsDemoApplication.java new file mode 100644 index 000000000..b3d5bfdbf --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/ExtensionsDemoApplication.java @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.rest.MyResource; + +/** + * Autoconfig mode sample application. + * + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +public class ExtensionsDemoApplication extends Application { + + public static void main(String[] args) throws Exception { + new ExtensionsDemoApplication().run("server"); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // direct resource registration + .extensions(MyResource.class) + // scan must find another resource + .enableAutoConfig("ru.vyarus.dropwizard.guice.examples.rest.scan") + // third resource bound in module + .modules(new AppModule()) + + // to show configured extensions + .printDiagnosticInfo() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResource.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResource.java new file mode 100644 index 000000000..5d5827ffb --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResource.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.AppModule; +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Resource instantiated by guice. {@link SampleService} interface implementation is configured in guice module + * {@link AppModule}. + * + * {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller} will force singleton + * for resource, so manual singleton definition is not required. + * + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@Path("/sample") +@Produces(MediaType.APPLICATION_JSON) +public class MyResource { + + @Inject + private SampleService service; + + @GET + @Path("/") + public Response latest() { + return Response.ok(service.foo()).build(); + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResourceFromBinding.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResourceFromBinding.java new file mode 100644 index 000000000..bdcfab775 --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/MyResourceFromBinding.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@Path("/sample3") +@Produces(MediaType.APPLICATION_JSON) +public class MyResourceFromBinding { + + @Inject + private SampleService service; + + @GET + @Path("/") + public Response latest() { + return Response.ok(service.foo()).build(); + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/scan/MyResourceFromScan.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/scan/MyResourceFromScan.java new file mode 100644 index 000000000..d28455d2d --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/scan/MyResourceFromScan.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.rest.scan; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@Path("/sample2") +@Produces(MediaType.APPLICATION_JSON) +public class MyResourceFromScan { + + @Inject + private SampleService service; + + @GET + @Path("/") + public Response latest() { + return Response.ok(service.foo()).build(); + } +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java new file mode 100644 index 000000000..2e6d58615 --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java @@ -0,0 +1,10 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +public interface SampleService { + + String foo(); +} diff --git a/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleServiceImpl.java b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleServiceImpl.java new file mode 100644 index 000000000..2551878fe --- /dev/null +++ b/examples/core-extensions/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleServiceImpl.java @@ -0,0 +1,16 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import jakarta.inject.Singleton; + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@Singleton +public class SampleServiceImpl implements SampleService { + + @Override + public String foo() { + return "foo"; + } +} diff --git a/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ClashedResourcesTest.groovy b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ClashedResourcesTest.groovy new file mode 100644 index 000000000..d7ffa0e73 --- /dev/null +++ b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ClashedResourcesTest.groovy @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@TestDropwizardApp(ExtensionsClashApplication) +class ClashedResourcesTest extends Specification { + + def "Check resource call"() { + + expect: "call resources" + new URL("http://localhost:8080/sample").getText() == "foo" + new URL("http://localhost:8080/sample2").getText() == "foo" + new URL("http://localhost:8080/sample3").getText() == "foo" + } +} diff --git a/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/GuiceyAppTest.groovy b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/GuiceyAppTest.groovy new file mode 100644 index 000000000..0563b1a40 --- /dev/null +++ b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/GuiceyAppTest.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.service.SampleService +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@TestGuiceyApp(ExtensionsDemoApplication) +class GuiceyAppTest extends Specification { + + @Inject + SampleService service + + def "Check guice service"() { + + expect: "service injected" + service.foo() == "foo" + } +} \ No newline at end of file diff --git a/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourcesTest.groovy b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourcesTest.groovy new file mode 100644 index 000000000..2b04bdaba --- /dev/null +++ b/examples/core-extensions/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourcesTest.groovy @@ -0,0 +1,20 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@TestDropwizardApp(ExtensionsDemoApplication) +class ResourcesTest extends Specification { + + def "Check resource call"() { + + expect: "call resources" + new URL("http://localhost:8080/sample").getText() == "foo" + new URL("http://localhost:8080/sample2").getText() == "foo" + new URL("http://localhost:8080/sample3").getText() == "foo" + } +} diff --git a/examples/core-getting-started/README.md b/examples/core-getting-started/README.md new file mode 100644 index 000000000..b13ebb4d8 --- /dev/null +++ b/examples/core-getting-started/README.md @@ -0,0 +1,15 @@ +### Getting started application + +Sources for application described in getting started documentation chapter. + +Shows: + +* Guice bundle definition +* Resource definition +* Managed usage +* Filter definition with web installers +* Custom guice module usage + * With 3rd party modules + * Accessing dropwizard objects from module + +Application does not use configuration, so no custom configuration class defined. \ No newline at end of file diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleApplication.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleApplication.java new file mode 100644 index 000000000..a13da4471 --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleApplication.java @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * @author Vyacheslav Rusakov + * @since 05.02.2017 + */ +public class SampleApplication extends Application { + + public static void main(String[] args) throws Exception { + new SampleApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .modules(new SampleModule()) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleModule.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleModule.java new file mode 100644 index 000000000..dbf171fe4 --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/SampleModule.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Configuration; +import ru.vyarus.dropwizard.guice.examples.modules.Some3rdPatyModule; +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; + +/** + * @author Vyacheslav Rusakov + * @since 12.02.2017 + */ +public class SampleModule extends DropwizardAwareModule { + + @Override + protected void configure() { + // 3rd party guice modules installation + install(new Some3rdPatyModule()); + + // example access to dropwizard objects from module + configuration(); + environment(); + bootstrap(); + } +} diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/modules/Some3rdPatyModule.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/modules/Some3rdPatyModule.java new file mode 100644 index 000000000..1d570b238 --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/modules/Some3rdPatyModule.java @@ -0,0 +1,15 @@ +package ru.vyarus.dropwizard.guice.examples.modules; + +import com.google.inject.AbstractModule; + +/** + * @author Vyacheslav Rusakov + * @since 12.02.2017 + */ +public class Some3rdPatyModule extends AbstractModule { + + @Override + protected void configure() { + + } +} diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java new file mode 100644 index 000000000..4c3246d36 --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 05.02.2017 + */ +@Path("/sample") +@Produces("application/json") +public class SampleResource { + + @Inject + private Provider requestProvider; + + @GET + @Path("/") + public Response ask() { + final String ip = requestProvider.get().getRemoteAddr(); + return Response.ok(ip).build(); + } +} diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleBootstrap.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleBootstrap.java new file mode 100644 index 000000000..550166024 --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleBootstrap.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.inject.Singleton; + +/** + * @author Vyacheslav Rusakov + * @since 05.02.2017 + */ +@Singleton +public class SampleBootstrap implements Managed { + private final Logger logger = LoggerFactory.getLogger(SampleBootstrap.class); + + @Override + public void start() throws Exception { + logger.info("Starting some resource"); + } + + @Override + public void stop() throws Exception { + logger.info("Shutting down some resource"); + } +} diff --git a/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AuthFilter.java b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AuthFilter.java new file mode 100644 index 000000000..52ccda23f --- /dev/null +++ b/examples/core-getting-started/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AuthFilter.java @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.examples.web; + +import jakarta.servlet.*; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 05.02.2017 + */ +@WebFilter(urlPatterns = "/*") +public class AuthFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if ("me".equals(request.getParameter("user"))) { + chain.doFilter(request, response); + } else { + ((HttpServletResponse) response) + .sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authorized"); + } + } + + @Override + public void destroy() { + } +} diff --git a/examples/core-getting-started/src/test/groovy/ru/vyarus/dropwizard/guice/example/SampleApplicationTest.groovy b/examples/core-getting-started/src/test/groovy/ru/vyarus/dropwizard/guice/example/SampleApplicationTest.groovy new file mode 100644 index 000000000..73e95c5cb --- /dev/null +++ b/examples/core-getting-started/src/test/groovy/ru/vyarus/dropwizard/guice/example/SampleApplicationTest.groovy @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.example + +import ru.vyarus.dropwizard.guice.examples.SampleApplication +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 06.03.2017 + */ +@TestDropwizardApp(SampleApplication) +class SampleApplicationTest extends Specification { + + def "Check application startup"() { + + when: "call resource directly" + new URL("http://localhost:8080/sample/").getText() + then: "not allowed" + def ex = thrown(IOException) + ex.message.startsWith('Server returned HTTP response code: 401 for URL') + + when: "call resource correctly" + def res = new URL("http://localhost:8080/sample/?user=me").getText() + then: "allowed" + res + } +} diff --git a/examples/core-installer-custom/README.md b/examples/core-installer-custom/README.md new file mode 100644 index 000000000..c8c90092a --- /dev/null +++ b/examples/core-installer-custom/README.md @@ -0,0 +1,44 @@ +### Custom installer implementation sample + +There may be many cases when [custom installer](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/installers/#writing-custom-installer) +need may arise. In all cases installer must replace some boilerplate. + +For example, + +* [JDBI3](http://xvik.github.io/dropwizard-guicey/5.0.0/extras/jdbi3) use custom installer to install +[repositories](http://xvik.github.io/dropwizard-guicey/5.0.0/extras/jdbi3/#repository) and +[row mappers](http://xvik.github.io/dropwizard-guicey/5.0.0/extras/jdbi3/#row-mapper) +* In [dropwizard-jobs integration example](../integration-dropwizard-jobs) custom installer used for jobs +registration + +In this example, custom installer detects extensions implementing `Marker` interface +and register guice-managed instances in `MarkersCollector` gucie bean (yes, normally such extension +must be done with guice multibindings, but its just for demonstration!) + +```java +bootstrap.addBundle(GuiceBundle.builder() + // if classpath scan enabled, installer could be detected automatically + .installers(MarkersInstaller.class) + .extensions(SampleMarker.class) +``` + +After startup you can see installer report: + +``` +INFO [2019-12-31 08:54:11,994] ru.vyarus.dropwizard.guice.examples.installer.MarkersInstaller: Installed markers = + + r.v.d.g.e.service.SampleMarker + +``` + +And, with enabled diagnostic report `.printDiagnosticInfo()`, we can see that extension is indeed +recognized by custom installer: + +``` + INSTALLERS and EXTENSIONS in processing order = + markers (r.v.d.g.e.installer.MarkersInstaller) + SampleMarker (r.v.d.g.e.service) +``` + +NOTE: installers shine with classpath scan because this way you don't need to even specify +extensions - installer would detect all classes matched implemented signs. \ No newline at end of file diff --git a/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/CustomInstallerApplication.java b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/CustomInstallerApplication.java new file mode 100644 index 000000000..0bd76fe92 --- /dev/null +++ b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/CustomInstallerApplication.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.installer.MarkersInstaller; +import ru.vyarus.dropwizard.guice.examples.service.SampleMarker; + +/** + * Sample application for custom installer in manual config mode. + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +public class CustomInstallerApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .installers(MarkersInstaller.class) + .extensions(SampleMarker.class) + // to show that marker was installed by custom installer + .printDiagnosticInfo() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/Marker.java b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/Marker.java new file mode 100644 index 000000000..9d1f227df --- /dev/null +++ b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/Marker.java @@ -0,0 +1,10 @@ +package ru.vyarus.dropwizard.guice.examples.installer; + +/** + * Marker interface to recognize extensions. + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +public interface Marker { +} diff --git a/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/MarkersInstaller.java b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/MarkersInstaller.java new file mode 100644 index 000000000..7215459e6 --- /dev/null +++ b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/installer/MarkersInstaller.java @@ -0,0 +1,45 @@ +package ru.vyarus.dropwizard.guice.examples.installer; + +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; +import ru.vyarus.dropwizard.guice.examples.service.MarkersCollector; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.InstanceInstaller; +import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; + +/** + * Installer recognize classes implementing {@link Marker} interface and sets guice-managed instance into + * {@link MarkersCollector} service. + *

        + * NOTE: this implementation is just an example of how to use gucie context within installer. Normally such cases + * should be using guice multibindings for collecting "plugins" (and entire set injection in target service). + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +public class MarkersInstaller implements FeatureInstaller, InstanceInstaller { + + private final Reporter reporter = new Reporter(MarkersInstaller.class, "Installed markers = "); + + @Override + public boolean matches(Class type) { + // recognize classes implementing interface + return FeatureUtils.is(type, Marker.class); + } + + @Override + public void install(Environment environment, Marker instance) { + // register instance in guice bean + InjectorLookup.getInstance(environment, MarkersCollector.class).get().register(instance); + // register instance for console report + reporter.line(RenderUtils.renderClass(instance.getClass())); + } + + @Override + public void report() { + // report all markers to console + reporter.report(); + } +} diff --git a/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/MarkersCollector.java b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/MarkersCollector.java new file mode 100644 index 000000000..3fada0c77 --- /dev/null +++ b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/MarkersCollector.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import ru.vyarus.dropwizard.guice.examples.installer.Marker; + +import jakarta.inject.Singleton; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +public class MarkersCollector { + + private final List markers = new ArrayList<>(); + + public void register(Marker marker) { + markers.add(marker); + } + + public List getMarkers() { + return markers; + } +} diff --git a/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleMarker.java b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleMarker.java new file mode 100644 index 000000000..009d04fae --- /dev/null +++ b/examples/core-installer-custom/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleMarker.java @@ -0,0 +1,17 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import ru.vyarus.dropwizard.guice.examples.installer.Marker; +import ru.vyarus.dropwizard.guice.examples.installer.MarkersInstaller; + +import jakarta.inject.Singleton; + +/** + * Service which must be recognized and installed by + * {@link MarkersInstaller}. + * + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +@Singleton +public class SampleMarker implements Marker { +} diff --git a/examples/core-installer-custom/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy b/examples/core-installer-custom/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy new file mode 100644 index 000000000..69e2dad4d --- /dev/null +++ b/examples/core-installer-custom/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples + +import com.google.inject.Injector +import com.google.inject.Key +import ru.vyarus.dropwizard.guice.examples.installer.MarkersInstaller +import ru.vyarus.dropwizard.guice.examples.service.SampleMarker +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2016 + */ +@TestGuiceyApp(CustomInstallerApplication) +class AppTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + @Inject + Injector injector + + def "Check feature installation"() { + + expect: "installer and feature registered" + info.installers.contains(MarkersInstaller) + injector.getExistingBinding(Key.get(SampleMarker)) != null + + } +} \ No newline at end of file diff --git a/examples/core-installers-reset/README.md b/examples/core-installers-reset/README.md new file mode 100644 index 000000000..736a089a5 --- /dev/null +++ b/examples/core-installers-reset/README.md @@ -0,0 +1,44 @@ +### Default installers re-cofniguration sample + +Guicey register many [installers](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/installers/) by default: + +``` + INSTALLERS in processing order = + OBJECT, ORDER lifecycle (r.v.d.g.m.i.f.LifeCycleInstaller) + OBJECT, ORDER managed (r.v.d.g.m.i.feature.ManagedInstaller) + OBJECT jerseyfeature (r.v.d.g.m.i.f.j.JerseyFeatureInstaller) + JERSEY, BIND, OPTIONS jerseyprovider (r.v.d.g.m.i.f.j.p.JerseyProviderInstaller) + TYPE, JERSEY, BIND, OPTIONS resource (r.v.d.g.m.i.f.j.ResourceInstaller) + BIND eagersingleton (r.v.d.g.m.i.f.e.EagerSingletonInstaller) + OBJECT healthcheck (r.v.d.g.m.i.f.h.HealthCheckInstaller) + OBJECT task (r.v.d.g.m.i.feature.TaskInstaller) + BIND plugin (r.v.d.g.m.i.f.plugin.PluginInstaller) + OBJECT, OPTIONS, ORDER webservlet (r.v.d.g.m.i.f.w.WebServletInstaller) + OBJECT, ORDER webfilter (r.v.d.g.m.i.f.web.WebFilterInstaller) + OBJECT, OPTIONS, ORDER weblistener (r.v.d.g.m.i.f.w.l.WebListenerInstaller) +``` + +But you can disable all of them with `.noDefaultInstallers()` and specify only +required installers. + +In this example, all installers except resource are disabled, so only rest resources +would be recognized and installed by guicey: + +```java +GuiceBundle.builder() + .noDefaultInstallers() + .installers(ResourceInstaller.class) + .extensions(SampleResource.class) + // see all registered installers + .printAvailableInstallers() + .build() +``` + +``` + INSTALLERS in processing order = + TYPE, JERSEY, BIND, OPTIONS resource (r.v.d.g.m.i.f.j.ResourceInstaller) +``` + + +Also see sample spock tests using both [GuiceyAppRule](https://github.com/xvik/dropwizard-guicey#testing) (start only guice context - very fast) and +[DropwizardAppRule](http://www.dropwizard.io/1.0.0/docs/manual/testing.html) (when http server started). \ No newline at end of file diff --git a/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/InstallersResetApplication.java b/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/InstallersResetApplication.java new file mode 100644 index 000000000..8f3cd015b --- /dev/null +++ b/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/InstallersResetApplication.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.rest.SampleResource; +import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller; + +/** + * Manual mode sample application. + * + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +public class InstallersResetApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // disable all installers except resources + .noDefaultInstallers() + .installers(ResourceInstaller.class) + + .extensions(SampleResource.class) + // see all registered installers + .printAvailableInstallers() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java b/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java new file mode 100644 index 000000000..4f7a86ee0 --- /dev/null +++ b/examples/core-installers-reset/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; + +/** + * Resource instantiated by guice. + *

        + * {@link ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller} will force singleton + * for resource, so manual singleton definition is not required. + * + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@Path("/sample") +@Produces("application/json") +public class SampleResource { + + @GET + @Path("/") + public Response latest() { + return Response.ok("foo").build(); + } +} diff --git a/examples/core-installers-reset/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy b/examples/core-installers-reset/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy new file mode 100644 index 000000000..034b9023f --- /dev/null +++ b/examples/core-installers-reset/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy @@ -0,0 +1,18 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@TestDropwizardApp(InstallersResetApplication) +class ResourceTest extends Specification { + + def "Check resource call"() { + + expect: "call resource" + new URL("http://localhost:8080/sample").getText() == "foo" + } +} diff --git a/examples/core-rest-sub-resource/README.md b/examples/core-rest-sub-resource/README.md new file mode 100644 index 000000000..2154a0071 --- /dev/null +++ b/examples/core-rest-sub-resource/README.md @@ -0,0 +1,136 @@ +## Sub resources + +Default resource installer in guicey registers resource instance, created with guice (resource is a guice bean). + +There are two ways of implementing jersey [sub resources](https://jersey.github.io/documentation/latest/jaxrs-resources.html#d0e2542) + +### Guice managed + +Sub resource is created as guice bean and it's instance returned from the resource method. + +```java +@Path("/root") +public class RootResource { + @Inject + private SubResource subResource; + + @Path("sub") + public SubResource sub() { + return subResource; + } +} + +@Singleton +public class SubResource { + + @GET + public String handle() { + return "guice"; + } +} +``` + +`RootResource` is installed by resource installer. `SubResource` is normal guice bean. + +Note that `SubResource` is not annotated with `@Path`. If it will be annotated and classpath scan will be enabled, +then sub resource should be annotated with `@InvisibleForScanner` to avoid sub resource installation as root resource. + +In order to access context parameters, jersey service must be used in sub resource: + +```java +@Path("/root") +public class RootResource { + @Inject + private SubResource subResource; + + @Path("{foo}/sub") + public SubResource sub() { + return subResource; + } +} + +@Singleton +public class SubResource { + + @Inject + private Provider uri; + + @GET + public String handle() { + String foo = uri.get().getPathParameters().getFirst("foo"); + return "guice " + foo; + } +} +``` + +Here sub resource access path parameter, declared on root resource. + +### Hk managed + +Last example could be more elegant with pure hk sub resource: + +```java +@Path("/root") +public class RootResource { + + @Path("{foo}/sub") + public Class sub() { + return SubResource.class; + } +} + +public class SubResource { + + private String foo; + + public HkSubResource(@PathParam("foo") String foo) { + this.foo = foo; + } + + @GET + public String handle() { + return "guice " + foo; + } +} +``` + +Now sub resource is managed by HK container and so can use all jersey features. But sub resource +instance will be created for each request. + +Note that if `SubResource` would be annotated with `@Path` it should be annotated with `@InvisibleForScanner` +to avoid installation (when classpath scan enabled). + +If guice dependencies required in sub resource then hk bridge [MUST be enabled](http://xvik.github.io/dropwizard-guicey/4.1.0/guide/configuration/#hk-bridge): + +Add dependency + +```groovy +implementation 'org.glassfish.hk2:guice-bridge' +``` + +Enable option + +```java +GuiceBundle.builder() + .option(GuiceyOptions.UseHkBridge, true) +``` + +Now guice services could be injected inside hk managed beans: + +```java +public class SubResource { + + private String foo; + private SomeGuiceService service; + + public HkSubResource(@PathParam("foo") String foo, SomeGuiceService service) { + this.foo = foo; + this.service = service; + } + + @GET + public String handle() { + return "guice " + service.handle(foo); + } +} +``` \ No newline at end of file diff --git a/examples/core-rest-sub-resource/build.gradle b/examples/core-rest-sub-resource/build.gradle new file mode 100644 index 000000000..0fb529141 --- /dev/null +++ b/examples/core-rest-sub-resource/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'org.glassfish.hk2:guice-bridge' +} \ No newline at end of file diff --git a/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/SubResourceApplication.java b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/SubResourceApplication.java new file mode 100644 index 000000000..af2fa0211 --- /dev/null +++ b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/SubResourceApplication.java @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.GuiceyOptions; + +/** + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +public class SubResourceApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + // bridge is required to inject guice service into hk managed sub resource + .option(GuiceyOptions.UseHkBridge, true) + // make sure service will not be created in both contexts (advanced validation for test) + .strictScopeControl() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/GuiceSubResource.java b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/GuiceSubResource.java new file mode 100644 index 000000000..9cbbaadaf --- /dev/null +++ b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/GuiceSubResource.java @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.core.UriInfo; + +/** + * Guice managed sub resource. Uses UriInfo jersey service to implement functionality from + * hk managed example. + * + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +@Singleton +public class GuiceSubResource { + + private Provider uri; + private SampleService service; + + @Inject + public GuiceSubResource(Provider uri, SampleService service) { + this.uri = uri; + this.service = service; + } + + @GET + public String handle() { + final String pathParamId = uri.get().getPathParameters().getFirst("pathParamId"); + return "guice " + service.applyState(pathParamId); + } +} diff --git a/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/HkSubResource.java b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/HkSubResource.java new file mode 100644 index 000000000..a6162636a --- /dev/null +++ b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/HkSubResource.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PathParam; + +/** + * Sub resource managed by HK. This is useful when advanced context binding is required: + * here pathParamId parameter is passed from the root resource. + *

        + * IMPORTANT: guice service injection will not work without enabled hk bridge (dependency + option). + * + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +public class HkSubResource { + private String id; + + private SampleService service; + + public HkSubResource(@PathParam("pathParamId") String id, SampleService service) { + this.id = id; + this.service = service; + } + + @GET + public String handle() { + return "hk " + service.applyState(id); + } +} diff --git a/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/RootResource.java b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/RootResource.java new file mode 100644 index 000000000..71ae98c43 --- /dev/null +++ b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/RootResource.java @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; + +/** + * Root resource is managed by guice. Two sub resources: one with guice and other, managed by hk (and created for + * each request). + * + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +@Path("/root") +public class RootResource { + + @Inject + private SampleService service; + @Inject + private GuiceSubResource subResource; + + @Path("{pathParamId}/guice-sub") + public GuiceSubResource guiceSubResource() { + service.setState("root1"); + // simply returning guice managed instance (singleton) + return subResource; + } + + @Path("{pathParamId}/hk-sub") + public Class hkSubResource() { + // use state to guarantee the same instance used in sub resource + service.setState("root2"); + // sub resource will be instantiated by hk2 + return HkSubResource.class; + } +} diff --git a/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java new file mode 100644 index 000000000..78da699f6 --- /dev/null +++ b/examples/core-rest-sub-resource/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import jakarta.inject.Singleton; + +/** + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +@Singleton +public class SampleService { + + // state used to show that service instance is the same for guice and sub-resource + private String state; + + public void setState(String state) { + this.state = state; + } + + public String applyState(String id) { + return state + " " + id; + } +} diff --git a/examples/core-rest-sub-resource/src/test/groovy/ru/vyarus/dropwizard/guice/examples/SubResAppTest.groovy b/examples/core-rest-sub-resource/src/test/groovy/ru/vyarus/dropwizard/guice/examples/SubResAppTest.groovy new file mode 100644 index 000000000..27310ad43 --- /dev/null +++ b/examples/core-rest-sub-resource/src/test/groovy/ru/vyarus/dropwizard/guice/examples/SubResAppTest.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 27.07.2017 + */ +@TestDropwizardApp(SubResourceApplication) +class SubResAppTest extends Specification { + + def "Check guice sub resource"() { + + when: "call guice managed sub resource" + def res = new URL("http://localhost:8080/root/12/guice-sub").getText() + then: "correct" + res == 'guice root1 12' + } + + def "Check hk sub resource"() { + + when: "call hk managed sub resource" + def res = new URL("http://localhost:8080/root/11/hk-sub").getText() + then: "correct" + res == 'hk root2 11' + } +} \ No newline at end of file diff --git a/examples/core-servlets/README.md b/examples/core-servlets/README.md new file mode 100644 index 000000000..4836afc94 --- /dev/null +++ b/examples/core-servlets/README.md @@ -0,0 +1,142 @@ +### Servlets and filters registration example + +There are multiple way to register [servlets and filters](http://xvik.github.io/dropwizard-guicey/5.0.0/guide/web/). + +#### Extensions + +The simplest is to use web installers, relying on web annotations: + +```java +@WebFilter("/sample/*") +@Singleton +public class SampleFilter extends HttpFilter { + + @Inject + SampleService service; + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + ... + } +} +``` + + +```java +@WebServlet("/sample") +@Singleton +public class SampleServlet extends HttpServlet { + + @Inject + private SampleService service; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ... + } +} +``` + +```java +@WebListener +@Singleton +public class SampleRequestListener implements ServletRequestListener { + + @Inject + private SampleService service; + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + ... + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + ... + } +} +``` + +For admin context registration only additional annotation must be added: + +```java +@Singleton +@WebServlet("/admin") +@AdminContext +public class AdminServlet extends HttpServlet { + ... +} +``` + +Extensions registration: + +```java +bootstrap.addBundle(GuiceBundle.builder() + // classpath scan or direct bindings (not in servlet module) could be used for registration instead + .extensions( + SampleServlet.class, + SampleFilter.class, + AdminServlet.class, + SampleRequestListener.class) +``` + +``` +http://localhost:8080/sample --- filter + servlet +http://localhost:8081/admin --- admin context servlet +``` + +#### Guice servlet module + +Guice filter is registered in both main and admin contexts and so all servlets and filters +registered in servlet module will apply to both contexts: + +```java +.modules(new ServletModule() { + @Override + protected void configureServlets() { + // note that if filter will be mapped as /gsample/* it will not apply to servlet call + serve("/gsample").with(GuiceServlet.class); + filter("/gsample").through(GuiceFilter.class); + } + }) +``` + +``` +http://localhost:8080/gsample --- filter + servlet +http://localhost:8081/gsample --- admin context servlet +``` + +Note that servlets and filters registered through `ServletModule` are not recognized +as guicey extension and so it would not be possible to disable exact servlet or filter +(with `.disableExtensions(...)`). + +#### Reporting + +All mapped servlets and filters could be easily seen on web report: + +```java +GuiceBundle.builder() + ... + .printWebMappings() +``` + +``` + MAIN / + ├── filter /sample/* SampleFilter (r.v.d.g.examples.web) [REQUEST] .sample + │ + ├── filter /* async GuiceFilter (c.g.inject.servlet) [REQUEST] Guice Filter + │ ├── guicefilter /gsample GuiceFilter r.v.d.g.e.ServletsDemoApplication$1 + │ └── guiceservlet /gsample GuiceServlet r.v.d.g.e.ServletsDemoApplication$1 + │ + ├── servlet /sample SampleServlet (r.v.d.g.examples.web) .sample + + + ADMIN / + │ + ├── filter /* async AdminGuiceFilter (r.v.d.g.m.i.internal) [REQUEST] Guice Filter + │ ├── guicefilter /gsample GuiceFilter r.v.d.g.e.ServletsDemoApplication$1 + │ └── guiceservlet /gsample GuiceServlet r.v.d.g.e.ServletsDemoApplication$1 + │ + ├── servlet /admin AdminServlet (r.v.d.g.examples.web) .admin + +``` \ No newline at end of file diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/ServletsDemoApplication.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/ServletsDemoApplication.java new file mode 100644 index 000000000..1c1e697b8 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/ServletsDemoApplication.java @@ -0,0 +1,49 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.google.inject.servlet.ServletModule; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.web.*; +import ru.vyarus.dropwizard.guice.examples.web.guice.GuiceFilter; +import ru.vyarus.dropwizard.guice.examples.web.guice.GuiceServlet; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +public class ServletsDemoApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // classpath scan or direct bindings (not in servlet module) could be used for registration instead + .extensions( + SampleServlet.class, + SampleFilter.class, + // just to show registration in admin context + AdminServlet.class, + SampleRequestListener.class) + + // registration with guice ServletModule + .modules(new ServletModule() { + @Override + protected void configureServlets() { + // note that if filter will be mapped as /gsample/* it will not apply to servlet call + serve("/gsample").with(GuiceServlet.class); + filter("/gsample").through(GuiceFilter.class); + } + }) + + // show web related registrations + .printWebMappings() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java new file mode 100644 index 000000000..dba643969 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import jakarta.inject.Singleton; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +public class SampleService { + + public String servletPart() { + return "srvlt"; + } + + public String filterPart() { + return "fltr"; + } + + public String listenerPart() { + return "listen!"; + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AdminServlet.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AdminServlet.java new file mode 100644 index 000000000..6700514a0 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/AdminServlet.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples.web; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; +import ru.vyarus.dropwizard.guice.module.installer.feature.web.AdminContext; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +@WebServlet("/admin") +@AdminContext +public class AdminServlet extends HttpServlet { + + @Inject + SampleService service; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().write(service.servletPart() + " admin"); + resp.flushBuffer(); + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleFilter.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleFilter.java new file mode 100644 index 000000000..ab1758433 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleFilter.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.examples.web; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@WebFilter("/sample/*") +@Singleton +public class SampleFilter extends HttpFilter { + + @Inject + SampleService service; + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + res.getWriter().write(service.filterPart() + " "); + chain.doFilter(req, res); + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleRequestListener.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleRequestListener.java new file mode 100644 index 000000000..70e5b9b49 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleRequestListener.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.examples.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.annotation.WebListener; + + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@WebListener +@Singleton +public class SampleRequestListener implements ServletRequestListener { + + private final Logger logger = LoggerFactory.getLogger(SampleRequestListener.class); + private int calls; + + @Inject + private SampleService service; + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + logger.info("{} request destroyed", service.listenerPart()); + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + logger.info("{} request initiated", service.listenerPart()); + calls++; + } + + public int getCalls() { + return calls; + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleServlet.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleServlet.java new file mode 100644 index 000000000..e552cbc92 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/SampleServlet.java @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples.web; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@WebServlet("/sample") +@Singleton +public class SampleServlet extends HttpServlet { + + @Inject + private SampleService service; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().write(service.servletPart()); + resp.flushBuffer(); + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceFilter.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceFilter.java new file mode 100644 index 000000000..a96dcd085 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceFilter.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.examples.web.guice; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +public class GuiceFilter extends HttpFilter { + + @Inject + private SampleService service; + + @Override + protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + res.getWriter().write(service.filterPart() + " guice "); + chain.doFilter(req, res); + } +} diff --git a/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceServlet.java b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceServlet.java new file mode 100644 index 000000000..4d4bd8758 --- /dev/null +++ b/examples/core-servlets/src/main/java/ru/vyarus/dropwizard/guice/examples/web/guice/GuiceServlet.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.web.guice; + +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@Singleton +public class GuiceServlet extends HttpServlet { + + @Inject + private SampleService service; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.getWriter().write(service.servletPart() + " guice"); + resp.flushBuffer(); + } +} diff --git a/examples/core-servlets/src/test/groovy/ru/vyarus/dropwizard/guice/examples/WebFeaturesTest.groovy b/examples/core-servlets/src/test/groovy/ru/vyarus/dropwizard/guice/examples/WebFeaturesTest.groovy new file mode 100644 index 000000000..175b08492 --- /dev/null +++ b/examples/core-servlets/src/test/groovy/ru/vyarus/dropwizard/guice/examples/WebFeaturesTest.groovy @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.web.SampleRequestListener +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.12.2019 + */ +@TestDropwizardApp(ServletsDemoApplication) +class WebFeaturesTest extends Specification { + + @Inject + SampleRequestListener listener + + def "Check registered servlets and filters execution"() { + + expect: "servlet and filter registered" + new URL("http://localhost:8080/sample").getText() == "fltr srvlt" + + and: "servlet module servlet and filter registered with guice module" + new URL("http://localhost:8080/gsample").getText() == "fltr guice srvlt guice" + + and: "guice servlets are visile in admin context" + new URL("http://localhost:8081/gsample").getText() == "fltr guice srvlt guice" + + and: "admin context servlet registered" + new URL("http://localhost:8081/admin").getText() == "srvlt admin" + + and: "listener detected 2 calls" + listener.calls == 2 + + } +} diff --git a/examples/ext-eventbus/README.md b/examples/ext-eventbus/README.md new file mode 100644 index 000000000..8e7f13697 --- /dev/null +++ b/examples/ext-eventbus/README.md @@ -0,0 +1,22 @@ +### Guava EventBus integration example + +Use [eventbus guicey extension](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-eventbus) to: +* automatically register listeners +* bind eventbus (for publication) +* print available listeners to console + +Installation: + +```java +GuiceBundle.builder() + .bundles(new EventBusBundle()) +``` + +[EventListener](src/main/java/ru/vyarus/dropwizard/guice/examples/service/EventListener.java) listens for +`FooEvent`, `BarEvent` and `BaseEvent` - base class for both events. + +Note that listeners inside `EventListener` registered because it's implicitly registered as guice bean by +injection in `SampleResource`. + +[SampleResource](src/main/java/ru/vyarus/dropwizard/guice/examples/resource/SampleResource.java) +used to trigger both events and receive overall calls stats. \ No newline at end of file diff --git a/examples/ext-eventbus/build.gradle b/examples/ext-eventbus/build.gradle new file mode 100644 index 000000000..b77843231 --- /dev/null +++ b/examples/ext-eventbus/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'ru.vyarus.guicey:guicey-eventbus' +} \ No newline at end of file diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/EventBusApp.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/EventBusApp.java new file mode 100644 index 000000000..08c54c278 --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/EventBusApp.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guicey.eventbus.EventBusBundle; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +public class EventBusApp extends Application { + + public static void main(String[] args) throws Exception { + new EventBusApp().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .bundles(new EventBusBundle()) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BarEvent.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BarEvent.java new file mode 100644 index 000000000..ec9e03503 --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BarEvent.java @@ -0,0 +1,8 @@ +package ru.vyarus.dropwizard.guice.examples.event; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +public class BarEvent extends BaseEvent { +} diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BaseEvent.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BaseEvent.java new file mode 100644 index 000000000..2f6689ecf --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/BaseEvent.java @@ -0,0 +1,8 @@ +package ru.vyarus.dropwizard.guice.examples.event; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +public abstract class BaseEvent { +} diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/FooEvent.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/FooEvent.java new file mode 100644 index 000000000..c94d7a683 --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/event/FooEvent.java @@ -0,0 +1,8 @@ +package ru.vyarus.dropwizard.guice.examples.event; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +public class FooEvent extends BaseEvent { +} diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/SampleResource.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/SampleResource.java new file mode 100644 index 000000000..62821df29 --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/SampleResource.java @@ -0,0 +1,50 @@ +package ru.vyarus.dropwizard.guice.examples.resource; + +import com.google.common.eventbus.EventBus; +import ru.vyarus.dropwizard.guice.examples.event.BarEvent; +import ru.vyarus.dropwizard.guice.examples.event.FooEvent; +import ru.vyarus.dropwizard.guice.examples.service.EventListener; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +@Path("/sample") +@Produces(MediaType.APPLICATION_JSON) +public class SampleResource { + + @Inject + private EventBus bus; + @Inject + private EventListener listener; + + @GET + @Path("/foo") + public int foo() { + bus.post(new FooEvent()); + return listener.getFooCalls(); + } + + + @GET + @Path("/bar") + public int bar() { + bus.post(new BarEvent()); + return listener.getBarCalls(); + } + + + @GET + @Path("/stats") + @Produces(MediaType.TEXT_PLAIN) + public String base() { + return String.format("Foo: %s, Bar: %s, Base: %s", + listener.getFooCalls(), listener.getBarCalls(), listener.getBaseCalls()); + } +} diff --git a/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/service/EventListener.java b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/service/EventListener.java new file mode 100644 index 000000000..b06779391 --- /dev/null +++ b/examples/ext-eventbus/src/main/java/ru/vyarus/dropwizard/guice/examples/service/EventListener.java @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import com.google.common.eventbus.Subscribe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.examples.event.BarEvent; +import ru.vyarus.dropwizard.guice.examples.event.BaseEvent; +import ru.vyarus.dropwizard.guice.examples.event.FooEvent; + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +public class EventListener { + + private Logger logger = LoggerFactory.getLogger(EventListener.class); + + private int base; + private int foo; + private int bar; + + @Subscribe + public void foo(FooEvent event) { + foo++; + logger.info("Foo event {} received: {}", foo, event); + } + + @Subscribe + public void bar(BarEvent event) { + bar++; + logger.info("Bar event {} received: {}", bar, event); + } + + @Subscribe + public void base(BaseEvent event) { + base++; + logger.info("Base event {} received: {}", base, event); + } + + public int getBaseCalls() { + return base; + } + + public int getFooCalls() { + return foo; + } + + public int getBarCalls() { + return bar; + } +} diff --git a/examples/ext-eventbus/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy b/examples/ext-eventbus/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy new file mode 100644 index 000000000..066efd585 --- /dev/null +++ b/examples/ext-eventbus/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AppTest.groovy @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 07.03.2017 + */ +@TestDropwizardApp(EventBusApp) +class AppTest extends Specification { + + def "Check event bus"() { + + when: "calling foo" + def res = new URL("http://localhost:8080/sample/foo").getText() + then: "ok" + res == "1" + + when: "calling bar" + res = new URL("http://localhost:8080/sample/bar").getText() + then: "ok" + res == "1" + + when: "check stats" + def stats = new URL("http://localhost:8080/sample/stats").getText() + then: "ok" + stats == 'Foo: 1, Bar: 1, Base: 2' + } +} \ No newline at end of file diff --git a/examples/ext-gsp-spa/README.md b/examples/ext-gsp-spa/README.md new file mode 100644 index 000000000..841d73b76 --- /dev/null +++ b/examples/ext-gsp-spa/README.md @@ -0,0 +1,38 @@ +### GSP SPA HTML5 routing sample + +This is an evolution of [SPA example](../ext-spa) but with dynamic base path. + +Use [GSP guicey module]((https://github.com/xvik/dropwizard-guicey/tree/master/guicey-server-pages)) +for index page to set correct base path. Note that gsp module extends spa module and so inherits its features. + +There are 3 examples: +1. Same as in SPA example: + * Run application + * Try http://localhost:8080/app -> index page will open + * Switch page (e.g. to /foo) + * Refresh browser -> index page must be loaded for route url (http://localhost:8080/app/foo) + + The difference is that index page is freemarker template and base path mapped dynamically: + ```html + + ``` + +2. Same static resources mapped on different url: `http://localhost:8080/app2` + +3. The same application, but index page mapped as rest view, so you can use custom computed properties in + the template. Note that index page mapped to rest path: + ```java + ServerPagesBundle.app("app3", "/app/", "/app3/") + // index page is now freemarker template + .mapViews("/views/app3/") // <-- map rest views for application + .indexPage("/index/") // <-- rest view path, not actual page + ``` + Template file is delcared in view class: + ```java + @Template("index3.ftl") + public class ComplexIndexView { + ``` + Try `http://localhost:8080/app3`: it would be the same index page with an additional line: + `

        Dynamic value: ${sample}

        ` + +NOTE: all apps need to explicitly enable SPA routing with `.spaRouting()` \ No newline at end of file diff --git a/examples/ext-gsp-spa/build.gradle b/examples/ext-gsp-spa/build.gradle new file mode 100644 index 000000000..1fcee8ac8 --- /dev/null +++ b/examples/ext-gsp-spa/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation 'ru.vyarus.guicey:guicey-server-pages' + implementation 'io.dropwizard:dropwizard-views-freemarker' +} \ No newline at end of file diff --git a/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/GspSpaApplication.java b/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/GspSpaApplication.java new file mode 100644 index 000000000..f9099bdc9 --- /dev/null +++ b/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/GspSpaApplication.java @@ -0,0 +1,60 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.view.ComplexIndexView; +import ru.vyarus.guicey.gsp.ServerPagesBundle; + +/** + * @author Vyacheslav Rusakov + * @since 22.10.2020 + */ +public class GspSpaApplication extends Application { + + public static void main(String[] args) throws Exception { + new GspSpaApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + bootstrap.addBundle(GuiceBundle.builder() + // view rest for example 3 + .extensions(ComplexIndexView.class) + .bundles( + // global views support + ServerPagesBundle.builder().build(), + // application registration + ServerPagesBundle.app("app", "/app/", "/app/") + // index page is now freemarker template + .indexPage("index.ftl") + .spaRouting() + .build(), + + // same application registration on different url + ServerPagesBundle.app("app2", "/app/", "/app2/") + // index page is now freemarker template + .indexPage("index.ftl") + .spaRouting() + .build(), + + // same application but with index page served as view + ServerPagesBundle.app("app3", "/app/", "/app3/") + // index page is now freemarker template + .mapViews("/views/app3/") // <-- map rest views for application + .indexPage("/index/") // <-- rest view path, not actual page + .spaRouting() + .build() + ) + .build()); + } + + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/view/ComplexIndexView.java b/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/view/ComplexIndexView.java new file mode 100644 index 000000000..7d5aba0ef --- /dev/null +++ b/examples/ext-gsp-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/view/ComplexIndexView.java @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.examples.view; + +import ru.vyarus.guicey.gsp.views.template.Template; +import ru.vyarus.guicey.gsp.views.template.TemplateView; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * @author Vyacheslav Rusakov + * @since 22.10.2020 + */ +@Path("/views/app3/index/") +@Produces(MediaType.TEXT_HTML) +// otherwise template could be specified in Model constructor (super) +// and in this case empty @Template marker would still be required +@Template("index3.ftl") +public class ComplexIndexView { + + @GET + public Model index() { + // just an example of computed model property + return new Model("sample string"); + } + + public static class Model extends TemplateView { + String sample; + + public Model(String sample) { + this.sample = sample; + } + + public String getSample() { + return sample; + } + } +} diff --git a/examples/ext-gsp-spa/src/main/resources/app/index.ftl b/examples/ext-gsp-spa/src/main/resources/app/index.ftl new file mode 100644 index 000000000..42ec4bc0a --- /dev/null +++ b/examples/ext-gsp-spa/src/main/resources/app/index.ftl @@ -0,0 +1,23 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> + + + + + + + Sample SPA + + + +
        +

        Sample routing

        +

        + Foo page | + Bar page +

        + +
        + + + + diff --git a/examples/ext-gsp-spa/src/main/resources/app/index3.ftl b/examples/ext-gsp-spa/src/main/resources/app/index3.ftl new file mode 100644 index 000000000..6d6c1204a --- /dev/null +++ b/examples/ext-gsp-spa/src/main/resources/app/index3.ftl @@ -0,0 +1,24 @@ +<#-- @ftlvariable name="" type="ru.vyarus.dropwizard.guice.examples.view.ComplexIndexView.Model" --> + + + + + + + Sample SPA + + + +
        +

        Sample routing

        +

        Dynamic value: ${sample}

        +

        + Foo page | + Bar page +

        + +
        + + + + diff --git a/examples/ext-gsp-spa/src/main/resources/app/script.js b/examples/ext-gsp-spa/src/main/resources/app/script.js new file mode 100644 index 000000000..450121661 --- /dev/null +++ b/examples/ext-gsp-spa/src/main/resources/app/script.js @@ -0,0 +1,16 @@ +'use strict'; + +const Foo = { template: '
        foo
        ' } +const Bar = { template: '
        bar
        ' } + +const router = new VueRouter({ + mode: 'history', + routes: [ + { path: '/foo', component: Foo }, + { path: '/bar', component: Bar } + ] +}); + +const app = new Vue({ + router +}).$mount('#app'); diff --git a/examples/ext-gsp-spa/src/main/resources/app/style.css b/examples/ext-gsp-spa/src/main/resources/app/style.css new file mode 100644 index 000000000..7249a416d --- /dev/null +++ b/examples/ext-gsp-spa/src/main/resources/app/style.css @@ -0,0 +1,8 @@ +html, body { + margin: 5px; + padding: 0; +} + +.router-link-active { + color: red; +} diff --git a/examples/ext-gsp-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy b/examples/ext-gsp-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy new file mode 100644 index 000000000..ed036cf30 --- /dev/null +++ b/examples/ext-gsp-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy @@ -0,0 +1,52 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 22.10.2020 + */ +@TestDropwizardApp(GspSpaApplication) +class RouteTest extends Specification { + + def "Check route url leads to html page"(ClientSupport client) { + + when: "loading index page" + def index = client.targetApp('app/').request().get(String) + then: "index loaded" + index.contains("") + + when: "loading route" + def route = client.targetApp('app/foo').request().accept('text/html').get(String) + then: "index loaded" + route == index + } + + def "Check app2 routing"(ClientSupport client) { + + when: "loading index page" + def index = client.targetApp('app2/').request().get(String) + then: "index loaded" + index.contains("") + + when: "loading route" + def route = client.targetApp('app2/foo').request().accept('text/html').get(String) + then: "index loaded" + route == index + } + + def "Check app3 routing"(ClientSupport client) { + + when: "loading index page" + def index = client.targetApp('app3/').request().get(String) + then: "index loaded" + index.contains("") + + when: "loading route" + def route = client.targetApp('app3/foo').request().accept('text/html').get(String) + then: "index loaded" + route == index + } +} diff --git a/examples/ext-gsp/README.md b/examples/ext-gsp/README.md new file mode 100644 index 000000000..34330736a --- /dev/null +++ b/examples/ext-gsp/README.md @@ -0,0 +1,46 @@ +### Guicey server pages example + +Use [GSP guicey module]((https://github.com/xvik/dropwizard-guicey/tree/master/guicey-server-pages)). + +Sample application configures app: + +```java +ServerPagesBundle.app("app", "/app/", "/") + // rest path as index page + .indexPage("person/") + .build() +``` + +(Application with name "app", with resources in classpath path "/app", served from root url) + +Note that index page set to resource-powered template ` .indexPage("person/")` + +Start application with arguments: + +``` +server config.yml +``` + +Static resource call: + +```java +http://localhost:8080/style.css → src/main/resources/com/example/app/style.css +``` + +Direct template call: + +``` +http://localhost:8080/foo.ftl → src/main/resources/com/example/app/foo.ftl +``` + +Rest-driven template call: + +``` +http://localhost:8080/person/12 → /rest/com.example.app/person/12 +``` + +Index page: + +``` +http://localhost:8080/ → /rest/com.example.app/person/ +``` \ No newline at end of file diff --git a/examples/ext-gsp/build.gradle b/examples/ext-gsp/build.gradle new file mode 100644 index 000000000..1fcee8ac8 --- /dev/null +++ b/examples/ext-gsp/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation 'ru.vyarus.guicey:guicey-server-pages' + implementation 'io.dropwizard:dropwizard-views-freemarker' +} \ No newline at end of file diff --git a/examples/ext-gsp/config.yml b/examples/ext-gsp/config.yml new file mode 100644 index 000000000..a8746de40 --- /dev/null +++ b/examples/ext-gsp/config.yml @@ -0,0 +1,3 @@ +server: + rootPath: '/rest/*' + applicationContextPath: / \ No newline at end of file diff --git a/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/GspApplication.java b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/GspApplication.java new file mode 100644 index 000000000..5241cb132 --- /dev/null +++ b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/GspApplication.java @@ -0,0 +1,39 @@ +package ru.vyarus.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guicey.gsp.ServerPagesBundle; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +public class GspApplication extends Application { + + public static void main(String[] args) throws Exception { + new GspApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .bundles( + // global views support + ServerPagesBundle.builder().build(), + // application registration + ServerPagesBundle.app("app", "/app/", "/") + // rest path as index page + .indexPage("person/") + .build()) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/dao/PersonDao.java b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/dao/PersonDao.java new file mode 100644 index 000000000..76b214a37 --- /dev/null +++ b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/dao/PersonDao.java @@ -0,0 +1,14 @@ +package ru.vyarus.guice.examples.dao; + +import ru.vyarus.guice.examples.model.Person; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +public class PersonDao { + + public Person find(int id) { + return new Person("John Doe " + id); + } +} diff --git a/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/model/Person.java b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/model/Person.java new file mode 100644 index 000000000..e43a35546 --- /dev/null +++ b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/model/Person.java @@ -0,0 +1,22 @@ +package ru.vyarus.guice.examples.model; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +public class Person { + + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonPage.java b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonPage.java new file mode 100644 index 000000000..ec20e4620 --- /dev/null +++ b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonPage.java @@ -0,0 +1,38 @@ +package ru.vyarus.guice.examples.ui.person; + +import ru.vyarus.guice.examples.dao.PersonDao; +import ru.vyarus.guicey.gsp.views.template.Template; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +// Path starts with application name +@Path("/app/person/") +@Produces(MediaType.TEXT_HTML) +// Important marker +@Template +public class PersonPage { + + @Inject + private PersonDao dao; + + @GET + @Path("/") + public PersonView getMaster() { + return new PersonView(dao.find(1)); + } + + @GET + @Path("/{id}") + public PersonView getPerson(@PathParam("id") Integer id) { + return new PersonView(dao.find(id)); + } +} diff --git a/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonView.java b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonView.java new file mode 100644 index 000000000..86448ac8d --- /dev/null +++ b/examples/ext-gsp/src/main/java/ru/vyarus/guice/examples/ui/person/PersonView.java @@ -0,0 +1,21 @@ +package ru.vyarus.guice.examples.ui.person; + +import ru.vyarus.guice.examples.model.Person; +import ru.vyarus.guicey.gsp.views.template.TemplateView; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +public class PersonView extends TemplateView { + private final Person person; + + public PersonView(Person person) { + super("person.ftl"); + this.person = person; + } + + public Person getPerson() { + return person; + } +} \ No newline at end of file diff --git a/examples/ext-gsp/src/main/resources/app/foo.ftl b/examples/ext-gsp/src/main/resources/app/foo.ftl new file mode 100644 index 000000000..e86d2e4f9 --- /dev/null +++ b/examples/ext-gsp/src/main/resources/app/foo.ftl @@ -0,0 +1,6 @@ +<#-- Sample template without model (/foo.ftl) --> + + +

        Hello, it's a template: ${12+22}!

        + + \ No newline at end of file diff --git a/examples/ext-gsp/src/main/resources/app/person.ftl b/examples/ext-gsp/src/main/resources/app/person.ftl new file mode 100644 index 000000000..8d1fd3a44 --- /dev/null +++ b/examples/ext-gsp/src/main/resources/app/person.ftl @@ -0,0 +1,10 @@ +<#-- Template with model, rendered by rest endpoint (/person/) --> +<#-- @ftlvariable name="" type="ru.vyarus.guice.examples.ui.person.PersonView" --> + + + + + +

        Hello, ${person.name}!

        + + \ No newline at end of file diff --git a/examples/ext-gsp/src/main/resources/app/style.css b/examples/ext-gsp/src/main/resources/app/style.css new file mode 100644 index 000000000..128fdf389 --- /dev/null +++ b/examples/ext-gsp/src/main/resources/app/style.css @@ -0,0 +1,4 @@ +html, body { + margin: 5px; + padding: 0; +} \ No newline at end of file diff --git a/examples/ext-gsp/src/test/groovy/ru/vyarus/dropwizard/examples/AppPathsTest.groovy b/examples/ext-gsp/src/test/groovy/ru/vyarus/dropwizard/examples/AppPathsTest.groovy new file mode 100644 index 000000000..001fc9e16 --- /dev/null +++ b/examples/ext-gsp/src/test/groovy/ru/vyarus/dropwizard/examples/AppPathsTest.groovy @@ -0,0 +1,37 @@ +package ru.vyarus.dropwizard.examples + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guice.examples.GspApplication +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +@TestDropwizardApp(value = GspApplication, restMapping = "/rest/*") +class AppPathsTest extends Specification { + + def "Check application urls"() { + + when: "call css file" + def res = new URL("http://localhost:8080/style.css").getText() + then: "correct" + res.contains("html, body {") + + when: "call direct template" + res = new URL("http://localhost:8080/foo.ftl").getText() + then: "ok" + res.contains("Hello, it's a template:") + + when: "call parametrized view" + res = new URL("http://localhost:8080/person/12").getText() + then: "ok" + res.contains("Hello, John Doe 12!") + + when: "call for index page" + res = new URL("http://localhost:8080/").getText() + then: "view rendered" + res.contains("Hello, John Doe 1!") + } + +} diff --git a/examples/ext-jdbi3/README.md b/examples/ext-jdbi3/README.md new file mode 100644 index 000000000..856f763f6 --- /dev/null +++ b/examples/ext-jdbi3/README.md @@ -0,0 +1,109 @@ +### JDBI3 integration sample + +Use [JDBI3 guicey extension](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-jdbi3) to: +* use jdbi proxies as guice beans +* be able to use injection inside proxies +* be able to use AOP on proxies +* use annotations for transaction definition +* automatic repositories and mapper installation + +[Dropwizard jdbi3 integration](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) is used to configure +and create jdbi instance. See [configuration](src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3AppConfiguration.java). + +For simplicity, embedded H2 database used. +Database scheme must be created before launching application. +[Dropwizard-flyway](https://github.com/dropwizard/dropwizard-flyway) used to prepare database (it's actually used only for [manual run](#manual-run) - +tests use flyway directly). See [db scheme](src/main/resources/db/migration/V1__setup.sql). + + +JDBI instance created exactly as described in [dropwizard docs](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) +using provided db configuration: + +```java +.bundles(JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())) +``` + +(You can provide pre-build dbi instance instead). + +`JdbiBundle` will activate additional installers. + +Note custom jdbi plugin installation for H2: + +```java +.withPlugins(new H2DatabasePlugin())) +``` + +#### Repository + +[Repositories installer](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-jdbi3#repository): all jdbi proxies must be annotated with `@JdbiRepository` so installer could recognize them. +See [UserRepository](src/main/java/ru/vyarus/dropwizard/guice/examples/repository/UserRepository.java) + +Repository is annotated with `@InTransaction` to allow using repositories directly: repository method call is the smallest transaction scope. +Transaction scope could be enlarged by using annotation on calling guice beans or +[declaring transaction manually](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-jdbi3#manual-transaction-definition). +In order to better understand how transactions work read [unit of work docs section](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-jdbi3#unit-of-work). + +Note that `InTransaction` is handled with guice AOP, so you can use any other guice aop related features. + +Repositories are restricted to interfaces, but use can declare custom logic in default methods. +In order to use guice beans inside default methods, "injection getters" must be used: + +```java + @Inject + RandomNameGenerator getGgenerator(); +``` + +As an extra demonstration, base repository class ([Crud](src/main/java/ru/vyarus/dropwizard/guice/examples/repository/Crud.java)) +implements hibernate-like optimistic lock concept: on each entity save version field is assigned/incremented and +checked during update to prevent data loss. + +#### Row mapper + +[Row mapper installer](https://github.com/xvik/dropwizard-guicey/tree/master/guicey-jdbi3#row-mapper): detects all implementations of `RowMapper`. + +Row mapper is used to map query result set to entity: [UserMapper](src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/UserMapper.java). +It's automatically registered in jdbi instance. Mapper are instantiated as normal guice beans without restrictions: so you can use injection and aop +(it's only not shown in example mapper). + +Also, see complementing [UserBind](src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/bind/UserBind.java) +annotation, used to bind object to query parameters: + +```java +@SqlUpdate("update users set version=:version, name=:name where id=:id and version=:version - 1") +int update(@UserBind User entry); +``` + +There is no custom installer for annotation because it's detected automatically by JDBI. + +#### Tests + +You can see in tests example of in-memory H2 db creation re-using application migration scripts. Flyway used in tests directly (bundle nto used). +Special bundle [FlywayInitBundle](src/test/groovy/ru/vyarus/dropwizard/guice/examples/util/FlywayInitBundle.groovy) is installed using +guicey [bundles lookup mechanism](https://github.com/xvik/dropwizard-guicey#bundle-lookup) (enabled by defualt): + +```groovy +PropertyBundleLookup.enableBundles(FlywayInitBundle) +``` + +### Manual run + +If you want to start application manually, use prepared [example-config.yml](example-config.yml). It's configured to use persistent databse, located at `~/sample`. + +First create schema: +``` +Jdbi3Application db migrate +``` + +Then run app normally + +``` +Jdbi3Application server example-config.yml +``` + +Note: it's psuedo-commands just to show application start parameters (assuming you will run main class form IDE). + +If you need to re-create databse use: +``` +Jdbi3Application db clean +Jdbi3Application db migrate +``` diff --git a/examples/ext-jdbi3/build.gradle b/examples/ext-jdbi3/build.gradle new file mode 100644 index 000000000..87c9047dc --- /dev/null +++ b/examples/ext-jdbi3/build.gradle @@ -0,0 +1,6 @@ +dependencies { + implementation 'ru.vyarus.guicey:guicey-jdbi3' + + implementation 'com.h2database:h2' + implementation 'io.dropwizard.modules:dropwizard-flyway:4.0.0-1' +} \ No newline at end of file diff --git a/examples/ext-jdbi3/example-config.yml b/examples/ext-jdbi3/example-config.yml new file mode 100644 index 000000000..400fa01bd --- /dev/null +++ b/examples/ext-jdbi3/example-config.yml @@ -0,0 +1,15 @@ +database: + driverClass: org.h2.Driver + user: sa + password: + url: jdbc:h2:~/sample + properties: + charSet: UTF-8 + maxWaitForConnection: 1s + validationQuery: "SELECT 1" + validationQueryTimeout: 3s + minSize: 8 + maxSize: 32 + checkConnectionWhileIdle: false + evictionInterval: 10s + minIdleTime: 1 minute \ No newline at end of file diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3AppConfiguration.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3AppConfiguration.java new file mode 100644 index 000000000..65fed685d --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3AppConfiguration.java @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.core.Configuration; +import io.dropwizard.db.DataSourceFactory; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public class Jdbi3AppConfiguration extends Configuration { + + @Valid + @NotNull + @JsonProperty + private DataSourceFactory database = new DataSourceFactory(); + + public DataSourceFactory getDatabase() { + return database; + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3Application.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3Application.java new file mode 100644 index 000000000..1ce07402d --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/Jdbi3Application.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.flyway.FlywayBundle; +import org.jdbi.v3.core.h2.H2DatabasePlugin; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guicey.jdbi3.JdbiBundle; + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public class Jdbi3Application extends Application { + + public static void main(String[] args) throws Exception { + new Jdbi3Application().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .bundles(JdbiBundle + .forDatabase((conf, env) -> conf.getDatabase()) + .withPlugins(new H2DatabasePlugin())) + .build()); + // used for manual run to init db + bootstrap.addBundle(new FlywayBundle() { + @Override + public PooledDataSourceFactory getDataSourceFactory(Jdbi3AppConfiguration configuration) { + return configuration.getDatabase(); + } + }); + } + + @Override + public void run(Jdbi3AppConfiguration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/IdEntity.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/IdEntity.java new file mode 100644 index 000000000..857110e9c --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/IdEntity.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.model; + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public abstract class IdEntity { + + private long id; + // for optlock mechanism usage see Crud.java (repository base) + private Integer version; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/User.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/User.java new file mode 100644 index 000000000..3b58ce67d --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/model/User.java @@ -0,0 +1,18 @@ +package ru.vyarus.dropwizard.guice.examples.model; + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public class User extends IdEntity { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/Crud.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/Crud.java new file mode 100644 index 000000000..909db1c70 --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/Crud.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.examples.repository; + +import ru.vyarus.dropwizard.guice.examples.model.IdEntity; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; + +import java.util.ConcurrentModificationException; + +/** + * @param entity type + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public interface Crud { + + @InTransaction + default T save(final T entry) { + // hibernate-like optimistic locking mechanism: provided entity must have the same version as in database + if (entry.getId() == 0) { + entry.setVersion(1); + entry.setId(insert(entry)); + } else { + final int ver = entry.getVersion(); + entry.setVersion(ver + 1); + if (update(entry) == 0) { + throw new ConcurrentModificationException(String.format( + "Concurrent modification for object %s %s version %s", + entry.getClass().getName(), entry.getId(), ver)); + } + } + return entry; + } + + long insert(T entry); + + int update(T entry); +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/UserRepository.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/UserRepository.java new file mode 100644 index 000000000..8eee789fd --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/UserRepository.java @@ -0,0 +1,55 @@ +package ru.vyarus.dropwizard.guice.examples.repository; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import ru.vyarus.dropwizard.guice.examples.model.User; +import ru.vyarus.dropwizard.guice.examples.repository.mapper.bind.UserBind; +import ru.vyarus.dropwizard.guice.examples.service.RandomNameGenerator; +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; + +import jakarta.inject.Inject; +import java.util.List; + +/** + * Recognized and installed by special installer, registered by jdbi bundle. + * Repository will be in singleton scope automatically. + *

        + * {@link ru.vyarus.guicey.jdbi3.tx.InTransaction} declares lowest transaction scope to be able to use repository + * without any extra tx definition (scope may be enlarged). + * + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +@JdbiRepository +@InTransaction +public interface UserRepository extends Crud { + + @Inject + RandomNameGenerator getGenerator(); + + // sample of hybrid method in repository, using injected service + default User createRandomUser() { + final User user = new User(); + user.setName(getGenerator().generateName()); + save(user); + return user; + } + + @Override + @SqlUpdate("insert into users (name, version) values (:name, :version)") + @GetGeneratedKeys + long insert(@UserBind User entry); + + @SqlUpdate("update users set version=:version, name=:name where id=:id and version=:version - 1") + @Override + int update(@UserBind User entry); + + @SqlQuery("select * from users") + List findAll(); + + @SqlQuery("select * from users where name = :name") + User findByName(@Bind("name") String name); +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/UserMapper.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/UserMapper.java new file mode 100644 index 000000000..06a7eac67 --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/UserMapper.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.repository.mapper; + +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; +import ru.vyarus.dropwizard.guice.examples.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Installed with special installer, registered by JDBI bundle. + * + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public class UserMapper implements RowMapper { + + @Override + public User map(ResultSet r, StatementContext ctx) throws SQLException { + User user = new User(); + user.setId(r.getLong("id")); + user.setVersion(r.getInt("version")); + user.setName(r.getString("name")); + return user; + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/bind/UserBind.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/bind/UserBind.java new file mode 100644 index 000000000..2bf8825ab --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/repository/mapper/bind/UserBind.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.examples.repository.mapper.bind; + +import org.jdbi.v3.core.statement.SqlStatement; +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory; +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizingAnnotation; +import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer; +import ru.vyarus.dropwizard.guice.examples.model.User; + +import java.lang.annotation.*; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; + +/** + * It's not installed by any guicey installer because DBI recognize annotations directly from usage. + * + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +@SqlStatementCustomizingAnnotation(UserBind.UserBinder.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface UserBind { + + class UserBinder implements SqlStatementCustomizerFactory { + + @Override + public SqlStatementParameterCustomizer createForParameter(Annotation annotation, + Class sqlObjectType, + Method method, + Parameter param, + int index, + Type paramType) { + return (stmt, obj) -> { + User arg = (User) obj; + ((SqlStatement) stmt) + .bind("id", arg.getId()) + .bind("version", arg.getVersion()) + .bind("name", arg.getName()); + }; + } + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/UserResource.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/UserResource.java new file mode 100644 index 000000000..a0a0fd8db --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/resource/UserResource.java @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.examples.resource; + +import com.google.common.base.Preconditions; +import ru.vyarus.dropwizard.guice.examples.model.User; +import ru.vyarus.dropwizard.guice.examples.repository.UserRepository; + +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +@Path("/users") +@Produces("application/json") +public class UserResource { + + private final UserRepository repository; + + @Inject + public UserResource(final UserRepository repository) { + this.repository = repository; + } + + @POST + @Path("/") + public User create(String name) { + Preconditions.checkState(repository.findByName(name) == null, + "User with name %s already exists", name); + User user = new User(); + user.setName(name); + return repository.save(user); + } + + @PUT + @Path("/") + public void update(User user) { + repository.save(user); + } + + @GET + @Path("/") + public List findAll() { + return repository.findAll(); + } +} diff --git a/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/service/RandomNameGenerator.java b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/service/RandomNameGenerator.java new file mode 100644 index 000000000..1160de653 --- /dev/null +++ b/examples/ext-jdbi3/src/main/java/ru/vyarus/dropwizard/guice/examples/service/RandomNameGenerator.java @@ -0,0 +1,15 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +/** + * User name rundomizer. Used to demonstrate guice service injection into repository. + * + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +public class RandomNameGenerator { + + public String generateName() { + // implementation doesn't matter + return "test" + Math.round(1000 * Math.random()); + } +} diff --git a/examples/ext-jdbi3/src/main/resources/db/migration/V1__setup.sql b/examples/ext-jdbi3/src/main/resources/db/migration/V1__setup.sql new file mode 100644 index 000000000..32d5853b4 --- /dev/null +++ b/examples/ext-jdbi3/src/main/resources/db/migration/V1__setup.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + id IDENTITY NOT NULL, + version INTEGER NOT NULL, + name VARCHAR, + CONSTRAINT users_id PRIMARY KEY (id) +); \ No newline at end of file diff --git a/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AbstractTest.groovy b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AbstractTest.groovy new file mode 100644 index 000000000..ea17665ac --- /dev/null +++ b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/AbstractTest.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup +import ru.vyarus.dropwizard.guice.examples.util.FlywayInitBundle +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +abstract class AbstractTest extends Specification { + static { + PropertyBundleLookup.enableBundles(FlywayInitBundle) + } + + @Inject + FlywayInitBundle.FlywaySupport flyway + + void setup() { + flyway.start() + } + + void cleanup() { + flyway.stop() + } +} \ No newline at end of file diff --git a/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserRepositoryTest.groovy b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserRepositoryTest.groovy new file mode 100644 index 000000000..284db79b1 --- /dev/null +++ b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserRepositoryTest.groovy @@ -0,0 +1,45 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.model.User +import ru.vyarus.dropwizard.guice.examples.repository.UserRepository +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +@TestGuiceyApp(value = Jdbi3Application, config = 'src/test/resources/test-config.yml') +class UserRepositoryTest extends AbstractTest { + + @Inject + UserRepository repository + + def "Check repository"() { + + expect: "repository actions" + repository.findAll().isEmpty() + + repository.save(new User(name: 'sample')) + repository.findAll().size() == 1 + + with(repository.findByName('sample')) { + id == 1 + version == 1 + name == 'sample' + } + } + + def "Check hybrid method"() { + + expect: "user created" + with(repository.createRandomUser()) { + id > 0 + version > 0 + name.startsWith('test') + repository.findByName(name) != null + } + + } +} diff --git a/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserResourceTest.groovy b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserResourceTest.groovy new file mode 100644 index 000000000..0dfa188fc --- /dev/null +++ b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/UserResourceTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.dropwizard.guice.examples + +import org.glassfish.jersey.client.JerseyClientBuilder +import ru.vyarus.dropwizard.guice.examples.model.User +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +import jakarta.ws.rs.client.Client +import jakarta.ws.rs.client.Entity +import jakarta.ws.rs.core.GenericType + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +@TestDropwizardApp(value = Jdbi3Application, config = 'src/test/resources/test-config.yml') +class UserResourceTest extends AbstractTest { + + + def "Check resource call"() { + + setup: + Client client = JerseyClientBuilder.createClient() + + when: "create user" + User res = client.target("http://localhost:8080/users").request() + .buildPost(Entity.text("sample")).invoke().readEntity(User.class) + then: "success" + res.id == 1 + res.version == 1 + res.name == 'sample' + + when: "modifying user" + res.name = "test" + client.target("http://localhost:8080/users").request() + .buildPut(Entity.json(res)).invoke() + List list = client.target("http://localhost:8080/users").request() + .buildGet().invoke().readEntity(new GenericType>() {}) + then: "modified" + list.size() == 1 + with(list[0]) { + name == "test" + id == 1 + version == 2 + } + } +} diff --git a/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/util/FlywayInitBundle.groovy b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/util/FlywayInitBundle.groovy new file mode 100644 index 000000000..0c88601b0 --- /dev/null +++ b/examples/ext-jdbi3/src/test/groovy/ru/vyarus/dropwizard/guice/examples/util/FlywayInitBundle.groovy @@ -0,0 +1,51 @@ +package ru.vyarus.dropwizard.guice.examples.util + +import io.dropwizard.db.DataSourceFactory +import io.dropwizard.lifecycle.Managed +import org.flywaydb.core.Flyway +import ru.vyarus.dropwizard.guice.examples.Jdbi3AppConfiguration +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.order.Order + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 01.11.2018 + */ +class FlywayInitBundle implements GuiceyBundle { + + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.extensions(FlywaySupport) + } + + @Order(Integer.MIN_VALUE) + static class FlywaySupport implements Managed { + + @Inject + Jdbi3AppConfiguration conf + Flyway flyway + + @Override + void start() throws Exception { + if (flyway != null) { + return + } + DataSourceFactory f = conf.getDatabase(); + flyway = Flyway.configure().cleanDisabled(false) + .dataSource(f.getUrl(), f.getUser(), f.getPassword()) + .load(); + flyway.migrate(); + } + + @Override + void stop() throws Exception { + if (flyway != null) { + flyway.clean() + flyway = null + } + } + } +} diff --git a/examples/ext-jdbi3/src/test/resources/test-config.yml b/examples/ext-jdbi3/src/test/resources/test-config.yml new file mode 100644 index 000000000..c2766ec5f --- /dev/null +++ b/examples/ext-jdbi3/src/test/resources/test-config.yml @@ -0,0 +1,5 @@ +database: + driverClass: org.h2.Driver + user: sa + password: + url: jdbc:h2:mem:test \ No newline at end of file diff --git a/examples/ext-spa/README.md b/examples/ext-spa/README.md new file mode 100644 index 000000000..52c1fe8e4 --- /dev/null +++ b/examples/ext-spa/README.md @@ -0,0 +1,25 @@ +### SPA HTML5 routing sample + +Use [SPA guicey module]((https://github.com/xvik/dropwizard-guicey/tree/master/guicey-spa)). + +Sample view application is in src/main/resources/app. Application use VuewRouter with two routes +: /foo and /bar. + +Application installed on /app context: + +```java +SpaBundle.app("app", "/app", "/app/") +``` + +(Application named "app" with resources located at "/app" on uri "/app/") + +NOTE that index.html sets base tag: + +```html + +``` + +* Run application +* Try http://localhost:8080/app -> index page will open +* Switch page (e.g. to /foo) +* Refresh browser -> index page must be loaded for route url (http://localhost:8080/app/foo) \ No newline at end of file diff --git a/examples/ext-spa/build.gradle b/examples/ext-spa/build.gradle new file mode 100644 index 000000000..8fdc2f8b0 --- /dev/null +++ b/examples/ext-spa/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'ru.vyarus.guicey:guicey-spa' +} \ No newline at end of file diff --git a/examples/ext-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/SpaApplication.java b/examples/ext-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/SpaApplication.java new file mode 100644 index 000000000..18f031a11 --- /dev/null +++ b/examples/ext-spa/src/main/java/ru/vyarus/dropwizard/guice/examples/SpaApplication.java @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guicey.spa.SpaBundle; + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +public class SpaApplication extends Application { + + public static void main(String[] args) throws Exception { + new SpaApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle.app("app", "/app", "/app/").build()) + .build()); + } + + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/ext-spa/src/main/resources/app/index.html b/examples/ext-spa/src/main/resources/app/index.html new file mode 100644 index 000000000..256e47db8 --- /dev/null +++ b/examples/ext-spa/src/main/resources/app/index.html @@ -0,0 +1,22 @@ + + + + + + + Sample SPA + + + +

        +

        Sample routing

        +

        + Foo page | + Bar page +

        + +
        + + + + diff --git a/examples/ext-spa/src/main/resources/app/script.js b/examples/ext-spa/src/main/resources/app/script.js new file mode 100644 index 000000000..450121661 --- /dev/null +++ b/examples/ext-spa/src/main/resources/app/script.js @@ -0,0 +1,16 @@ +'use strict'; + +const Foo = { template: '
        foo
        ' } +const Bar = { template: '
        bar
        ' } + +const router = new VueRouter({ + mode: 'history', + routes: [ + { path: '/foo', component: Foo }, + { path: '/bar', component: Bar } + ] +}); + +const app = new Vue({ + router +}).$mount('#app'); diff --git a/examples/ext-spa/src/main/resources/app/style.css b/examples/ext-spa/src/main/resources/app/style.css new file mode 100644 index 000000000..7249a416d --- /dev/null +++ b/examples/ext-spa/src/main/resources/app/style.css @@ -0,0 +1,8 @@ +html, body { + margin: 5px; + padding: 0; +} + +.router-link-active { + color: red; +} diff --git a/examples/ext-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy b/examples/ext-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy new file mode 100644 index 000000000..137f04d7c --- /dev/null +++ b/examples/ext-spa/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RouteTest.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 23.10.2019 + */ +@TestDropwizardApp(SpaApplication) +class RouteTest extends Specification { + + def "Check route url leads to html page"(ClientSupport client) { + + when: "loading index page" + def index = client.targetApp('app/').request().get(String) + then: "index loaded" + index.contains("") + + when: "loading route" + def route = client.targetApp('app/foo').request().accept('text/html').get(String) + then: "index loaded" + route == index + } +} diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7f93135c4 Binary files /dev/null and b/examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..0aaefbcaf --- /dev/null +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/gradlew b/examples/gradlew new file mode 100755 index 000000000..0adc8e1a5 --- /dev/null +++ b/examples/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/gradlew.bat b/examples/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/examples/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/integration-auth/README.md b/examples/integration-auth/README.md new file mode 100644 index 000000000..b91b8a577 --- /dev/null +++ b/examples/integration-auth/README.md @@ -0,0 +1,80 @@ +### Dropwizard authentication configuration example + +[Documentation](http://xvik.github.io/dropwizard-guicey/4.2.2/examples/authentication/) describes +dropwizard auth module usage in general. This example app shows OAuth configuration only. + +Dropwizard-auth module required: `implementation 'io.dropwizard:dropwizard-auth` + +`OAuthDynamicFeature` is installed automatically by classpath scan. + +```java +@Singleton +@Provider +public class OAuthDynamicFeature extends AuthDynamicFeature { + + @Inject + public OAuthDynamicFeature(UserAuthenticator authenticator, + UserAuthorizer authorizer, + Environment environment) { + super(new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(authenticator) + .setAuthorizer(authorizer) + .setPrefix("Bearer") + .buildAuthFilter()); + + environment.jersey().register(RolesAllowedDynamicFeature.class); + environment.jersey().register(new AuthValueFactoryProvider.Binder(User.class)); + } +} +``` + +Auth configuration is almost the same as in dropwizard documentation. + +Authenticator and Authorizer are simple guice beans. In the example beans are not described in +guice module - registration will be automatic on first injection request +(when `OAuthDynamicFeature` will be instantiated). + +Check token - provide user object +```java +@Singleton +public class UserAuthenticator implements Authenticator { + + @Override + public Optional authenticate(String credentials) throws AuthenticationException { + return Optional.ofNullable("valid".equals(credentials) ? new User("admin", "ADMIN") : null); + } +} +``` + +`@RolesAllowed` annotation support - check if authorized user contains role: +```java +@Singleton +public class UserAuthorizer implements Authorizer { + @Override + public boolean authorize(User user, String role) { + return user.getRoles().contains(role); + } +} +``` + + +Usage in resources is exactly as described in dropwizard guide: +```java + // authorization required (or 401 error) + @GET + @Path("/auth") + public String auth(@Auth User user) { + return user.getName(); + } + + // authorized user must have ADMIN role (or 403 error) + @GET + @Path("/adm") + @RolesAllowed("ADMIN") + public String admin(@Auth User user) { + return user.getName(); + } +``` + +Also see sample spock tests using both [GuiceyAppRule](https://github.com/xvik/dropwizard-guicey#testing) (start only guice context - very fast) and +[DropwizardAppRule](http://www.dropwizard.io/1.0.0/docs/manual/testing.html) (when http server started). \ No newline at end of file diff --git a/examples/integration-auth/build.gradle b/examples/integration-auth/build.gradle new file mode 100644 index 000000000..d7aceb10c --- /dev/null +++ b/examples/integration-auth/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'io.dropwizard:dropwizard-auth' +} \ No newline at end of file diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/AuthApplication.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/AuthApplication.java new file mode 100644 index 000000000..02a47b3d8 --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/AuthApplication.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * Application with OAuth authorization. + * + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +public class AuthApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/OAuthDynamicFeature.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/OAuthDynamicFeature.java new file mode 100644 index 000000000..f1466ca04 --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/OAuthDynamicFeature.java @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.examples.auth; + +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; +import io.dropwizard.core.setup.Environment; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.ext.Provider; + +/** + * Configure OAuth authentication, almost the same way as described in dropwizard guide. + * Note that authorizer and authenticator are guice beans (they will be initialized by guice on first injection + * request). + * + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +@Singleton +@Provider +// will be installed by JerseyProviderInstaller +public class OAuthDynamicFeature extends AuthDynamicFeature { + + @Inject + public OAuthDynamicFeature(UserAuthenticator authenticator, + UserAuthorizer authorizer, + Environment environment) { + super(new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(authenticator) + .setAuthorizer(authorizer) + .setPrefix("Bearer") + .buildAuthFilter()); + + environment.jersey().register(RolesAllowedDynamicFeature.class); + environment.jersey().register(new AuthValueFactoryProvider.Binder(User.class)); + } + +} \ No newline at end of file diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/User.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/User.java new file mode 100644 index 000000000..414099193 --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/User.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.examples.auth; + +import java.security.Principal; +import java.util.Arrays; +import java.util.List; + +/** + * Authenticated user. + * + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +public class User implements Principal { + + private String name; + private List roles; + + public User(String name, String... roles) { + this.name = name; + this.roles = Arrays.asList(roles); + } + + @Override + public String getName() { + return name; + } + + public List getRoles() { + return roles; + } +} diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthenticator.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthenticator.java new file mode 100644 index 000000000..d2cc6999b --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthenticator.java @@ -0,0 +1,23 @@ +package ru.vyarus.dropwizard.guice.examples.auth; + +import io.dropwizard.auth.AuthenticationException; +import io.dropwizard.auth.Authenticator; + +import jakarta.inject.Singleton; +import java.util.Optional; + +/** + * Validate credentials and provide user object. + * Guice bean. + * + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +@Singleton +public class UserAuthenticator implements Authenticator { + + @Override + public Optional authenticate(String credentials) throws AuthenticationException { + return Optional.ofNullable("valid".equals(credentials) ? new User("admin", "ADMIN") : null); + } +} \ No newline at end of file diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthorizer.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthorizer.java new file mode 100644 index 000000000..8ac6a2073 --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/auth/UserAuthorizer.java @@ -0,0 +1,21 @@ +package ru.vyarus.dropwizard.guice.examples.auth; + +import io.dropwizard.auth.Authorizer; +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestContext; +import org.jspecify.annotations.Nullable; + +/** + * Checks if authorized user has required role ({@link jakarta.annotation.security.RolesAllowed} annotation support). + * Guice bean. + * + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +@Singleton +public class UserAuthorizer implements Authorizer { + @Override + public boolean authorize(User user, String role, @Nullable ContainerRequestContext requestContext) { + return user.getRoles().contains(role); + } +} \ No newline at end of file diff --git a/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/AuthAwareResource.java b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/AuthAwareResource.java new file mode 100644 index 000000000..5b6923905 --- /dev/null +++ b/examples/integration-auth/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/AuthAwareResource.java @@ -0,0 +1,41 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import io.dropwizard.auth.Auth; +import ru.vyarus.dropwizard.guice.examples.auth.User; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +/** + * @author Vyacheslav Rusakov + * @since 25.01.2019 + */ +@Path("/") +@Produces("text/plain") +public class AuthAwareResource { + + // authorization required + @GET + @Path("/auth") + public String auth(@Auth User user) { + return user.getName(); + } + + // authorized user must have ADMIN role + @GET + @Path("/adm") + @RolesAllowed("ADMIN") + public String admin(@Auth User user) { + return user.getName(); + } + + // authorized user must have MRX role + @GET + @Path("/deny") + @RolesAllowed("MRX") + public String deny(@Auth User user) { + return user.getName(); + } +} diff --git a/examples/integration-auth/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy b/examples/integration-auth/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy new file mode 100644 index 000000000..f7aa1964d --- /dev/null +++ b/examples/integration-auth/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ResourceTest.groovy @@ -0,0 +1,61 @@ +package ru.vyarus.dropwizard.guice.examples + + +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +import jakarta.ws.rs.core.HttpHeaders + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2016 + */ +@TestDropwizardApp(AuthApplication) +class ResourceTest extends Specification { + + def "Check auth"(ClientSupport client) { + + when: "calling resource without auth" + def res = client.targetApp('/adm').request().get() + then: "user not authorized" + res.status == 401 + + + when: "calling resource without auth" + res = client.targetApp('/auth').request().get() + then: "user authorized" + res.status == 401 + + + when: "calling resource with incorrect auth" + res = client.targetApp('/adm').request() + .header(HttpHeaders.AUTHORIZATION, "Bearer invalid").get() + then: "user not authorized" + res.status == 401 + + + when: "calling resource with proper auth and role" + res = client.targetApp('/adm').request() + .header(HttpHeaders.AUTHORIZATION, "Bearer valid").get() + then: "user authorized" + res.status == 200 + res.readEntity(String) == 'admin' + + + when: "calling resource required auth" + res = client.targetApp('/auth').request() + .header(HttpHeaders.AUTHORIZATION, "Bearer valid").get() + then: "user authorized" + res.status == 200 + res.readEntity(String) == 'admin' + + + when: "calling resource using user without proper role" + res = client.targetApp('/deny').request() + .header(HttpHeaders.AUTHORIZATION, "Bearer valid").get() + then: "user not authorized" + res.status == 403 + + } +} diff --git a/examples/integration-dropwizard-jobs/README.md b/examples/integration-dropwizard-jobs/README.md new file mode 100644 index 000000000..7b7df81bf --- /dev/null +++ b/examples/integration-dropwizard-jobs/README.md @@ -0,0 +1,103 @@ +### Dropwizard-jobs example + +Example show [dropwizard-jobs](https://github.com/dropwizard-jobs/dropwizard-jobs) 3rd party library integration. + +NOTE: this is very basic integration. Normally it deserves special ext module, but it's not yet exists. + +#### Setup + +Add dropwizard-jobs dependency: + +```groovy +dependencies { + implementation 'io.github.dropwizard-jobs:dropwizard-jobs-guice:4.0.0-RELEASE' +} +``` + +#### Integration + +App configuration class must implement JobsConfiguration: + +```java +public class JobsAppConfiguration extends Configuration implements JobConfiguration +``` + +Library already provides guice integration, but it couldn't be used directly as it requires immediate +injector presence. Instead, we will use two guicey extensions. + +Note that application will search and install extensions using classpath scan: `.enableAutoConfig(JobsApplication.class.getPackage().getName())` + +First we define Managed to manage scheduler lifecycle: + +```java +@Singleton +public class JobsManager extends GuiceJobManager { + + @Inject + public JobsManager(Injector injector, JobsAppConfiguration configuration) { + super(configuration, injector); + } +} +``` + +It is recognized as Managed and installed automatically. Internally it will lookup all Job bindings in guice context +and register all found jobs. + +To avoid manual jobs registration we will use custom installer: + +```java +public class JobsInstaller implements FeatureInstaller, TypeInstaller { + + private final Reporter reporter = new Reporter(JobsInstaller.class, "jobs ="); + + @Override + public boolean matches(Class type) { + return FeatureUtils.is(type, Job.class); + } + + @Override + public void install(Environment environment, Class type) { + // here we can also look for class annotations and show more info in console + // (omitted for simplicity) + reporter.line("(%s)", type.getName()); + } + + @Override + public void report() { + reporter.report(); + } +} +``` + +It will be recognized and registered automatically. Installer performs two tasks: find job beans and bind to guice context (implicitly) +and print all found jobs to console. + +And the final step is configure dopwizard metrics registry in the application class: + +```java +@Override +public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(JobsApplication.class.getPackage().getName()) + .build()); + + // force dropwizard-jobs using main metrics registry for all jobs + SharedMetricRegistries.add(Job.DROPWIZARD_JOBS_KEY, bootstrap.getMetricRegistry()); +} +``` + +#### Sample job + +Now we can just declare jobs: + +```java +@Singleton +@Every("1s") +public class SampleJob extends Job { + + @Override + public void doJob(JobExecutionContext context) throws JobExecutionException { + ... + } +} +``` diff --git a/examples/integration-dropwizard-jobs/build.gradle b/examples/integration-dropwizard-jobs/build.gradle new file mode 100644 index 000000000..faca32634 --- /dev/null +++ b/examples/integration-dropwizard-jobs/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'io.github.dropwizard-jobs:dropwizard-jobs-core' +} \ No newline at end of file diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsAppConfiguration.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsAppConfiguration.java new file mode 100644 index 000000000..98256fa9f --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsAppConfiguration.java @@ -0,0 +1,21 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.jobs.JobConfiguration; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +public class JobsAppConfiguration extends JobConfiguration { + + @JsonProperty("quartz") + private Map quartz = new HashMap<>(); + + public Map getQuartzConfiguration() { + return quartz; + } +} diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsApplication.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsApplication.java new file mode 100644 index 000000000..4d678dba1 --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/JobsApplication.java @@ -0,0 +1,34 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.codahale.metrics.SharedMetricRegistries; +import io.dropwizard.core.Application; +import io.dropwizard.jobs.Job; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; + +/** + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +public class JobsApplication extends Application { + + public static void main(String[] args) throws Exception { + new JobsApplication().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .build()); + + // force dropwizard-jobs using main metrics registry for all jobs + SharedMetricRegistries.add(Job.DROPWIZARD_JOBS_KEY, bootstrap.getMetricRegistry()); + } + + @Override + public void run(JobsAppConfiguration configuration, Environment environment) throws Exception { + + } +} diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/job/SampleJob.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/job/SampleJob.java new file mode 100644 index 000000000..53e29344d --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/job/SampleJob.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.job; + +import io.dropwizard.jobs.Job; +import io.dropwizard.jobs.annotations.Every; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import jakarta.inject.Singleton; + +/** + * Job will be registered automatically by {@link ru.vyarus.dropwizard.guice.examples.support.JobsInstaller} + * + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +@Singleton +@Every("1s") +public class SampleJob extends Job { + + public boolean iDidIt; + + @Override + public void doJob(JobExecutionContext context) throws JobExecutionException { + iDidIt = true; + } +} diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/GuiceJobFactory.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/GuiceJobFactory.java new file mode 100644 index 000000000..418165000 --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/GuiceJobFactory.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.support; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import org.quartz.Job; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.spi.JobFactory; +import org.quartz.spi.TriggerFiredBundle; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class GuiceJobFactory implements JobFactory { + + private final Injector injector; + + @Inject + public GuiceJobFactory(final Injector injector) { + this.injector = injector; + } + + @Override + public Job newJob(final TriggerFiredBundle bundle, final Scheduler scheduler) throws SchedulerException { + return injector.getInstance(bundle.getJobDetail().getJobClass()); + } +} diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsInstaller.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsInstaller.java new file mode 100644 index 000000000..d2357a6e7 --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsInstaller.java @@ -0,0 +1,55 @@ +package ru.vyarus.dropwizard.guice.examples.support; + +import com.google.common.base.Preconditions; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.multibindings.Multibinder; +import io.dropwizard.jobs.Job; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.binding.BindingInstaller; +import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; + +/** + * Installer performs utility tasks: + * - searches for jobs and bind them to guice context (so {@link JobsManager} could install them + * - print registered jobs to console + * + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +public class JobsInstaller implements FeatureInstaller, BindingInstaller { + + private final Reporter reporter = new Reporter(JobsInstaller.class, "jobs ="); + + @Override + public boolean matches(Class type) { + return FeatureUtils.is(type, Job.class); + } + + @Override + @SuppressWarnings("unchecked") + public void bind(Binder binder, Class type, boolean lazy) { + Preconditions.checkArgument(!lazy, "Job bean can't be lazy: %s", type.getName()); + registerJob(binder, (Class) type); + } + + @Override + @SuppressWarnings("unchecked") + public void manualBinding(Binder binder, Class type, Binding binding) { + registerJob(binder, (Class) type); + } + + @Override + public void report() { + reporter.report(); + } + + private void registerJob(final Binder binder, final Class type) { + Multibinder.newSetBinder(binder, Job.class).addBinding().to(type); + + // here we can also look for class annotations and show more info in console + // (omitted for simplicity) + reporter.line("(%s)", type.getName()); + } +} diff --git a/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsManager.java b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsManager.java new file mode 100644 index 000000000..6e0f9167f --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/main/java/ru/vyarus/dropwizard/guice/examples/support/JobsManager.java @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.examples.support; + +import com.google.inject.Injector; +import io.dropwizard.jobs.Job; +import io.dropwizard.jobs.JobManager; +import org.quartz.spi.JobFactory; +import ru.vyarus.dropwizard.guice.examples.JobsAppConfiguration; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.ArrayList; +import java.util.Set; + +/** + * Bean will be recognized as Managed and installed automatically. + * Note that native dropwizard-jobs-guice module is NOT used because it scans entire injector whereas all + * jobs are revealed by the installer. Also, since dropwizard-jobs 5.1 guice module depends on guice 7 which can't be + * used with dropwizard 3. + * + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +@Singleton +public class JobsManager extends JobManager { + + private final GuiceJobFactory factory; + + @Inject + public JobsManager(final Injector injector, final Set jobs, final JobsAppConfiguration config) { + super(config, new ArrayList<>(jobs)); + this.factory = new GuiceJobFactory(injector); + } + + @Override + protected JobFactory getJobFactory() { + return factory; + } +} diff --git a/examples/integration-dropwizard-jobs/src/test/groovy/ru/vyarus/dropwizard/guice/examples/JobsAppTest.groovy b/examples/integration-dropwizard-jobs/src/test/groovy/ru/vyarus/dropwizard/guice/examples/JobsAppTest.groovy new file mode 100644 index 000000000..d0cfb7d84 --- /dev/null +++ b/examples/integration-dropwizard-jobs/src/test/groovy/ru/vyarus/dropwizard/guice/examples/JobsAppTest.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.job.SampleJob +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 11.03.2018 + */ +@TestGuiceyApp(JobsApplication) +class JobsAppTest extends Specification { + + @Inject + SampleJob job + + def "Check task execution"() { + + expect: "task called" + job.iDidIt + } +} diff --git a/examples/integration-guice-validator/README.md b/examples/integration-guice-validator/README.md new file mode 100644 index 000000000..e852ad3db --- /dev/null +++ b/examples/integration-guice-validator/README.md @@ -0,0 +1,154 @@ +### Guice-validator example + +Example show [guice-validator](https://github.com/xvik/guice-validator) 3rd party library integration. + +By default, dropwizard support jakarta.validation annotations usage on [rest resources](http://www.dropwizard.io/1.2.2/docs/manual/validation.html). +Guice-validator will allow using them on all guice beans and write custom guice-aware validators. + +NOTE: Integration deserves special ext module, but it's not exists now (planned), so pure example only. + +#### Setup + +Add guice-validator dependency: + +```groovy +dependencies { + implementation 'ru.vyarus:guice-validator:2.0.0' +} +``` + +Next we need to add validator guice module which will find and apply aop interceptors for +all validation annotations in guice beans: + +```java +.modules( + new ValidationModule(bootstrap.getValidatorFactory()) + .targetClasses(Matchers.not(Matchers.annotatedWith(Path.class))) + .targetMethods(Matchers.not(Matchers.annotatedWith(Path.class)))) +) +``` + +Note that dropwizard already applies validation to reast reasource, but they are also +guice beans and so to avoid duplication excluding all resource classes from "guice-validator scope". + +And the last thing is to substitute validator: + +```java +environment.setValidator(InjectorLookup.getInjector(this).get().getInstance(Validator.class)); +``` + +Explanation (you may skip this): dropwizard declares ValidatorFactory in bootstrap object. This factory is used then (on run phase) +to create Validator (used for validation). Guice-validator is also using dropwizard factory, but it has to +configure different ConstraintFactory, so validators creation could be delegated to guice (and so you can use injection in validators). +The problem here is that this configuration "forks" factory so dropwizard still use it's factory, but +guice will use "forked" factory. As the result, dropwizard creates Validator from it's factory, which is not aware of guice. +And guice use Validator, created from "forked" factory. In this case, custom guice-aware validators (custom annotations) will not +work on resources. To fix this we simply set correct validator to dropwizard environment. + + +#### Examples + +Custom constraint: we want to use custom validation annotation `@CustomCondition` to apply some +custom validation logic. + +```java +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CustomValidator.class}) +@Documented +public @interface CustomCondition { + + String message() default "Very specific case check failed"; + Class[] groups() default {}; + Class[] payload() default {}; +} +``` + +```java +public class CustomValidator implements ConstraintValidator { + + @Inject + SomeService service; + + @Override + public void initialize(CustomCondition constraintAnnotation) {} + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return /* do some check and return true/false */; + } +} +``` + +Usage in guice bean: + +```java +@Singleton +public class SomeService { + + public String customValidationParameter(@CustomCondition String value) ... + + @CustomCondition + public String customValidationReturn(String value) ... +``` + +When one of these methods would be called, `CustomValidator` will be created with guice and used +for validation automatically. + +The same way it will work on resources: + +```java +@GET +@Path("/custom") +public String withCustomValidator(@CustomCondition @QueryParam("q") String something) ... +``` + +#### Bean example + +In some cases it could be handy to tie custom validator for entity type (so you can be sure +that entity is always valid): + +```java +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {MyBeanValidator.class}) +@Documented +public @interface MyBeanValid { + + String message() default "Bean is not valid"; + Class[] groups() default {}; + Class[] payload() default {}; +} +``` + +```java +public class MyBeanValidator implements ConstraintValidator { + + @Override + public void initialize(MyBeanValid constraintAnnotation) {} + + @Override + public boolean isValid(MyBean value, ConstraintValidatorContext context) { + return /* some checks here (could be multiple checks) */; + } +} +``` + +Adding validation to bean: + +```java +@MyBeanValid +public class MyBean { ... +``` + +Now every time this bean appear under `@Valid` annotation, custom validator will work: + +```java +public void customBeanCheck(@Valid MyBean bean) { ... +``` + + +Read more in [guice-validator](https://github.com/xvik/guice-validator) documentation (for guice-related aspects) and +[hibernate-validator](http://hibernate.org/validator/) documentation for jakarta.validation description. + +See [Test](src/test/groovy/ru/vyarus/dropwizard/guice/examples/RestValidationTest.groovy) to make sure it works \ No newline at end of file diff --git a/examples/integration-guice-validator/build.gradle b/examples/integration-guice-validator/build.gradle new file mode 100644 index 000000000..408012c68 --- /dev/null +++ b/examples/integration-guice-validator/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'ru.vyarus:guice-validator' +} \ No newline at end of file diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/GValApplication.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/GValApplication.java new file mode 100644 index 000000000..96905bf39 --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/GValApplication.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.google.inject.matcher.Matchers; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.guice.validator.ValidationModule; + +import jakarta.validation.Validator; +import jakarta.ws.rs.Path; + +/** + * Guice-validator integration sample application. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +public class GValApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .modules( + // register validation module, but with exclusion for rest resources (which are guice beans) + // because dropwizard already applies validation support there + new ValidationModule(bootstrap.getValidatorFactory()) + .targetClasses(Matchers.not(Matchers.annotatedWith(Path.class))) + .targetMethods(Matchers.not(Matchers.annotatedWith(Path.class))) + ) + .build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + // substitute dropwizard validator with guice-aware validator in order to use custom + // validators in resources + environment.setValidator(InjectorLookup.getInjector(this).get().getInstance(Validator.class)); + } +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/ValResource.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/ValResource.java new file mode 100644 index 000000000..05e6bc7fd --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/ValResource.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import ru.vyarus.dropwizard.guice.examples.validator.CustomCondition; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; + +/** + * Dropwizard performs method validations. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@Path("/val") +@Produces("application/json") +public class ValResource { + + + // simple validation + @GET + @Path("/q") + public String doStaff(@NotNull @QueryParam("q") String something) { + return "done"; + } + + // validation with custom guice-aware validator + @GET + @Path("/custom") + public String withCustomValidator(@CustomCondition @QueryParam("q") String something) { + return "done"; + } +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SomeService.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SomeService.java new file mode 100644 index 000000000..a97477817 --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SomeService.java @@ -0,0 +1,43 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import ru.vyarus.dropwizard.guice.examples.validator.CustomCondition; +import ru.vyarus.dropwizard.guice.examples.validator.bean.MyBean; + +import jakarta.inject.Singleton; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * Sample service to show validation usage. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@Singleton +public class SomeService { + + // use existing jakarta.validation annotation + public String simpleValidation(@NotNull String value) { + return value; + } + + // use custom validator for parameter + public String customValidationParameter(@CustomCondition String value) { + return value; + } + + // use custom validator for return value + @CustomCondition + public String customValidationReturn(String value) { + return value; + } + + + // @Valid trigger @MyBeanValid declared on bean + public void customBeanCheck(@Valid MyBean bean) { + } + + public String getSomething() { + return "foo"; + } +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomCondition.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomCondition.java new file mode 100644 index 000000000..bc3f15735 --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomCondition.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.examples.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +/** + * Custom validation annotation for method parameter. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {CustomValidator.class}) +@Documented +public @interface CustomCondition { + + // could be localization key + String message() default "Very specific case check failed"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomValidator.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomValidator.java new file mode 100644 index 000000000..27b62208c --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/CustomValidator.java @@ -0,0 +1,30 @@ +package ru.vyarus.dropwizard.guice.examples.validator; + +import ru.vyarus.dropwizard.guice.examples.service.SomeService; + +import jakarta.inject.Inject; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * Validator used for {@link CustomCondition} anotation. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +public class CustomValidator implements ConstraintValidator { + + @Inject + SomeService service; + + @Override + public void initialize(CustomCondition constraintAnnotation) { + // annotation without parameters - no need for processing + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // use guice bean for validation + return service.getSomething().equals(value); + } +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBean.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBean.java new file mode 100644 index 000000000..42873dcc1 --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBean.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.examples.validator.bean; + +/** + * Example of custom bean with attached validator. + * Validator will be activated by {@link jakarta.validation.Valid} annotation. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@MyBeanValid +public class MyBean { + + private String foo; + private String bar; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValid.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValid.java new file mode 100644 index 000000000..5b65937fe --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValid.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.validator.bean; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +/** + * Annotation to validate {@link MyBean} bean type. Will be used on bean directly, but it could be used + * as validation annotation for method too (when not declared on bean). + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {MyBeanValidator.class}) +@Documented +public @interface MyBeanValid { + + // could be localization key + String message() default "Bean is not valid"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValidator.java b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValidator.java new file mode 100644 index 000000000..a1c9ade9d --- /dev/null +++ b/examples/integration-guice-validator/src/main/java/ru/vyarus/dropwizard/guice/examples/validator/bean/MyBeanValidator.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.examples.validator.bean; + +import ru.vyarus.dropwizard.guice.examples.service.SomeService; + +import jakarta.inject.Inject; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * Validator used to validate {@link MyBean}. + * + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +public class MyBeanValidator implements ConstraintValidator { + + @Inject + SomeService service; + + @Override + public void initialize(MyBeanValid constraintAnnotation) { + // no custom parameters + } + + @Override + public boolean isValid(MyBean value, ConstraintValidatorContext context) { + return service.getSomething().equals(value.getFoo()); + } +} diff --git a/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RestValidationTest.groovy b/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RestValidationTest.groovy new file mode 100644 index 000000000..2a626146c --- /dev/null +++ b/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/RestValidationTest.groovy @@ -0,0 +1,48 @@ +package ru.vyarus.dropwizard.guice.examples + +import org.glassfish.jersey.client.JerseyClientBuilder +import org.glassfish.jersey.client.JerseyWebTarget +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +import jakarta.ws.rs.BadRequestException +import jakarta.ws.rs.client.Client + +/** + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@TestDropwizardApp(GValApplication) +class RestValidationTest extends Specification { + + def "Check rest methods validation"() { + setup: + Client client = JerseyClientBuilder.createClient() + JerseyWebTarget target = client.target("http://localhost:8080/val/") + + when: "call method with simple validation" + String res = target.path('q').queryParam('q', 'foo') + .request().buildGet().invoke(String.class); + then: "result success" + res == 'done' + + when: "call method with simple validation without param" + target.path('q') + .request().buildGet().invoke(String.class); + then: "result fail" + thrown(BadRequestException) + + + when: "call method with custom validation" + res = target.path('custom').queryParam('q', 'foo') + .request().buildGet().invoke(String.class); + then: "result success" + res == 'done' + + when: "call method with custom validation without param" + target.path('custom').queryParam('q', 'bad') + .request().buildGet().invoke(String.class); + then: "result fail, guice enabled validator works" + thrown(BadRequestException) + } +} \ No newline at end of file diff --git a/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ServiceValidationTest.groovy b/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ServiceValidationTest.groovy new file mode 100644 index 000000000..b797cb222 --- /dev/null +++ b/examples/integration-guice-validator/src/test/groovy/ru/vyarus/dropwizard/guice/examples/ServiceValidationTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.dropwizard.guice.examples + +import ru.vyarus.dropwizard.guice.examples.service.SomeService +import ru.vyarus.dropwizard.guice.examples.validator.bean.MyBean +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject +import jakarta.validation.ConstraintViolationException + +/** + * @author Vyacheslav Rusakov + * @since 12.01.2018 + */ +@TestGuiceyApp(GValApplication) +class ServiceValidationTest extends Specification { + + @Inject + SomeService service + + static { + Locale.setDefault(Locale.ENGLISH) + } + + def "Check method validation success"() { + + when: "simple validation" + service.simpleValidation("foo") + then: "ok" + true + + when: "custom validation on parameter" + service.customValidationParameter(service.getSomething()) + then: "ok" + true + + when: "custom validation on return" + service.customValidationReturn(service.getSomething()) + then: "ok" + true + + when: "custom bean validation" + service.customBeanCheck(new MyBean(foo: service.getSomething())) + then: "ok" + true + } + + def "Check method validation fail"() { + + when: "simple validation" + service.simpleValidation(null) + then: "err" + def ex = thrown(ConstraintViolationException) + ex.constraintViolations.first().message == 'must not be null' + + when: "custom validation on parameter" + service.customValidationParameter('bee') + then: "err" + ex = thrown(ConstraintViolationException) + ex.constraintViolations.first().message == 'Very specific case check failed' + + when: "custom validation on return" + service.customValidationReturn('bee') + then: "err" + ex = thrown(ConstraintViolationException) + ex.constraintViolations.first().message == 'Very specific case check failed' + + when: "custom bean validation" + service.customBeanCheck(new MyBean(foo: 'bee')) + then: "err" + ex = thrown(ConstraintViolationException) + ex.constraintViolations.first().message == 'Bean is not valid' + } +} \ No newline at end of file diff --git a/examples/integration-hibernate/README.md b/examples/integration-hibernate/README.md new file mode 100644 index 000000000..319ea59d4 --- /dev/null +++ b/examples/integration-hibernate/README.md @@ -0,0 +1,89 @@ +### Hibernate integration sample + +[dropwiard-hibernate](http://www.dropwizard.io/1.0.0/docs/manual/hibernate.html) is configured exactly as + it's described in docs, but extracted to [separate class](src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnBundle.java) for simplicity: + + ```java + public class HbnBundle extends HibernateBundle { + + public HbnBundle() { + super(Sample.class); + } + + @Override + public PooledDataSourceFactory getDataSourceFactory(HbnAppConfiguration configuration) { + return configuration.getDataSourceFactory(); + } + } + ``` + + [Guice module](src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnModule.java) + used to provide SessionFactory instance into guice context: + + ```java + public class HbnModule extends AbstractModule { + + private final HbnBundle hbnBundle; + + public HbnModule(HbnBundle hbnBundle) { + this.hbnBundle = hbnBundle; + } + + @Override + protected void configure() { + bind(SessionFactory.class).toProvider(hbnBundle::getSessionFactory); + } + } + ``` + + And in [application](src/main/java/ru/vyarus/dropwizard/guice/examples/HbnApplication.java) init: + + ```java + @Override + public void initialize(Bootstrap bootstrap) { + final HbnBundle hibernate = new HbnBundle(); + // register hbn bundle before guice to make sure factory initialized before guice context start + bootstrap.addBundle(hibernate); + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig("ru.vyarus.dropwizard.guice.examples") + .modules(new HbnModule(hibernate)) + .build()); + } + ``` + +Overall it's a complete example with [one entity](src/main/java/ru/vyarus/dropwizard/guice/examples/model/Sample.java) +and [simple resource](src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java). + +[Test](src/test/groovy/ru/vyarus/dropwizard/guice/examples/HbnResourceTest.groovy) starts application +with in-memory h2 db ([see config](src/test/resources/config.yml)). + +#### Session in servlet or filter + +If you need to access hibernate in servlet or filter you will need to manage session manually. +For example: + +```java +@WebFilter("/*") +@Singleton +public class MyFilter implments Filter { + + @Inject + private SessionFactory sessionFactory; + + @Override + public void doFilter(ServletRequest servletRequest, + ServletResponse servletResponse, + FilterChain filterChain) + throws IOException, ServletException { + + Session session = sessionFactory.openSession(); + ManagedSessionContext.bind(session); + try { + // session opened, hibernate could be used + } finally { + ManagedSessionContext.unbind(sessionFactory); + session.close(); + } + } +} +``` \ No newline at end of file diff --git a/examples/integration-hibernate/build.gradle b/examples/integration-hibernate/build.gradle new file mode 100644 index 000000000..d35fc9685 --- /dev/null +++ b/examples/integration-hibernate/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation 'io.dropwizard:dropwizard-hibernate' + implementation 'com.h2database:h2' +} \ No newline at end of file diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnAppConfiguration.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnAppConfiguration.java new file mode 100644 index 000000000..3f4b524b5 --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnAppConfiguration.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.examples; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.core.Configuration; +import io.dropwizard.db.DataSourceFactory; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * Minimal configuration required for hibernate bundle. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +public class HbnAppConfiguration extends Configuration { + @Valid + @NotNull + @JsonProperty + private DataSourceFactory database = new DataSourceFactory(); + + public DataSourceFactory getDataSourceFactory() { + return database; + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnApplication.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnApplication.java new file mode 100644 index 000000000..9cc16cc6d --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/HbnApplication.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.hbn.HbnBundle; +import ru.vyarus.dropwizard.guice.examples.hbn.HbnModule; + +/** + * Application demonstrates dropwizard hibernate bundle integration with guice. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +public class HbnApplication extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + final HbnBundle hibernate = new HbnBundle(); + // register hbn bundle before guice to make sure factory initialized before guice context start + bootstrap.addBundle(hibernate); + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .modules(new HbnModule(hibernate)) + .build()); + } + + @Override + public void run(HbnAppConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnBundle.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnBundle.java new file mode 100644 index 000000000..42dc674e6 --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnBundle.java @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.examples.hbn; + +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.hibernate.HibernateBundle; +import ru.vyarus.dropwizard.guice.examples.HbnAppConfiguration; +import ru.vyarus.dropwizard.guice.examples.model.Sample; + +/** + * Configured dropwizard hibernate bundle. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +public class HbnBundle extends HibernateBundle { + + public HbnBundle() { + super(Sample.class); + } + + @Override + public PooledDataSourceFactory getDataSourceFactory(HbnAppConfiguration configuration) { + return configuration.getDataSourceFactory(); + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnModule.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnModule.java new file mode 100644 index 000000000..b1bdf820d --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/hbn/HbnModule.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.hbn; + +import com.google.inject.AbstractModule; +import org.hibernate.SessionFactory; + +/** + * Guice module for {@link SessionFactory} binding. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +public class HbnModule extends AbstractModule { + + private final HbnBundle hbnBundle; + + public HbnModule(HbnBundle hbnBundle) { + this.hbnBundle = hbnBundle; + } + + @Override + protected void configure() { + // if hibernate bundle was registered before guice, then at this point it's run method + // will be already called and so it's safe to get session factory instance + bind(SessionFactory.class).toProvider(hbnBundle::getSessionFactory); + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/model/Sample.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/model/Sample.java new file mode 100644 index 000000000..12ed45ea2 --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/model/Sample.java @@ -0,0 +1,53 @@ +package ru.vyarus.dropwizard.guice.examples.model; + +import jakarta.persistence.*; + +/** + * Sample hibernate entity. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +@Entity +@Table(name = "Sample") +public class Sample { + + @Id + @GeneratedValue + private long id; + @Version + private long version; + + private String name; + + public Sample() { + } + + public Sample(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java new file mode 100644 index 000000000..116753c00 --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/rest/SampleResource.java @@ -0,0 +1,40 @@ +package ru.vyarus.dropwizard.guice.examples.rest; + +import com.codahale.metrics.annotation.Timed; +import com.google.inject.Inject; +import io.dropwizard.hibernate.UnitOfWork; +import ru.vyarus.dropwizard.guice.examples.model.Sample; +import ru.vyarus.dropwizard.guice.examples.service.SampleService; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; +import java.util.List; + +/** + * Sample of hibernate usage. + * Sample method creates entity and return all db entities, so for each request resulted collection would be N+1. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +@Path("/sample") +@Produces("application/json") +public class SampleResource { + + @Inject + private SampleService service; + + @GET + @Path("/") + @Timed + @UnitOfWork + public Response doStaff() { + final Sample sample = new Sample("sample"); + service.create(sample); + final List res = service.findAll(); + // using response to render entities inside unit of work and avoid lazy load exceptions + return Response.ok(res).build(); + } +} diff --git a/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java new file mode 100644 index 000000000..88fb86386 --- /dev/null +++ b/examples/integration-hibernate/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import com.google.inject.Inject; +import io.dropwizard.hibernate.AbstractDAO; +import org.hibernate.SessionFactory; +import ru.vyarus.dropwizard.guice.examples.model.Sample; +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; + +import java.util.List; + +/** + * Sample service, buld using dropwizard dao abstraction. + *

        + * NOTE: @EagerSingleton is not required here, but used to force bean instance creation with guice context + * in order to demonstrate session factory availability. + * + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +@EagerSingleton +public class SampleService extends AbstractDAO { + + @Inject + public SampleService(final SessionFactory factory) { + super(factory); + } + + public long create(Sample sample) { + return persist(sample).getId(); + } + + @SuppressWarnings("unchecked") + public List findAll() { + return list(currentSession().createQuery("from Sample")); + } +} diff --git a/examples/integration-hibernate/src/test/groovy/ru/vyarus/dropwizard/guice/examples/HbnResourceTest.groovy b/examples/integration-hibernate/src/test/groovy/ru/vyarus/dropwizard/guice/examples/HbnResourceTest.groovy new file mode 100644 index 000000000..127e63363 --- /dev/null +++ b/examples/integration-hibernate/src/test/groovy/ru/vyarus/dropwizard/guice/examples/HbnResourceTest.groovy @@ -0,0 +1,39 @@ +package ru.vyarus.dropwizard.guice.examples + +import org.glassfish.jersey.client.JerseyClientBuilder +import org.glassfish.jersey.client.JerseyInvocation +import ru.vyarus.dropwizard.guice.examples.model.Sample +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +import jakarta.ws.rs.client.Client +import jakarta.ws.rs.core.GenericType + +/** + * @author Vyacheslav Rusakov + * @since 12.06.2016 + */ +@TestDropwizardApp(value = HbnApplication, config = 'src/test/resources/config.yml') +class HbnResourceTest extends Specification { + + def "Check resource call"() { + + setup: + Client client = JerseyClientBuilder.createClient() + JerseyInvocation get = client.target("http://localhost:8080/sample").request().buildGet() + + when: "call hbn resource" + List res = get.invoke(new GenericType>() { + }) + then: "result success" + res.size() == 1 + res.get(0).name == 'sample' + + when: "call hbn resource one more time" + res = get.invoke(new GenericType>() { + }) + then: "+1 result successfully returned" + res.size() == 2 + res.get(1).name == 'sample' + } +} \ No newline at end of file diff --git a/examples/integration-hibernate/src/test/resources/config.yml b/examples/integration-hibernate/src/test/resources/config.yml new file mode 100644 index 000000000..44be18a80 --- /dev/null +++ b/examples/integration-hibernate/src/test/resources/config.yml @@ -0,0 +1,10 @@ +database: + driverClass: org.h2.Driver + user: sa + password: + url: jdbc:h2:mem:sample + + properties: + charSet: UTF-8 + hibernate.dialect: org.hibernate.dialect.H2Dialect + hibernate.hbm2ddl.auto: create diff --git a/examples/maven-bom/.mvn/wrapper/maven-wrapper.jar b/examples/maven-bom/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/examples/maven-bom/.mvn/wrapper/maven-wrapper.jar differ diff --git a/examples/maven-bom/.mvn/wrapper/maven-wrapper.properties b/examples/maven-bom/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..6d3a56651 --- /dev/null +++ b/examples/maven-bom/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/examples/maven-bom/README.md b/examples/maven-bom/README.md new file mode 100644 index 000000000..f39d62566 --- /dev/null +++ b/examples/maven-bom/README.md @@ -0,0 +1,4 @@ +# BOM-based maven project + +Dropwizard-guicey declared with guicey BOM. +Sample junit 5 test. \ No newline at end of file diff --git a/examples/maven-bom/build.gradle b/examples/maven-bom/build.gradle new file mode 100644 index 000000000..5fe18ebd7 --- /dev/null +++ b/examples/maven-bom/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.github.dkorotych.gradle-maven-exec' +} + +// disable java compilation in gradle +tasks.withType(JavaCompile).configureEach { + enabled = false +} +tasks.withType(ProcessResources).configureEach { + enabled = false +} +tasks.withType(Jar).configureEach { + enabled = false +} + +tasks.register('maven-test', MavenExec) { + goals 'test' + options { + settings = rootProject.file('maven-settings.xml') + define=[ 'dropwizard-guicey.version':rootProject.ext.guiceyBom, 'dropwizard.version': rootProject.ext.dwVersion] + } +} + +tasks.build.dependsOn('maven-test') \ No newline at end of file diff --git a/examples/maven-bom/mvnw b/examples/maven-bom/mvnw new file mode 100755 index 000000000..8d937f4c1 --- /dev/null +++ b/examples/maven-bom/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/maven-bom/mvnw.cmd b/examples/maven-bom/mvnw.cmd new file mode 100644 index 000000000..c4586b564 --- /dev/null +++ b/examples/maven-bom/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/examples/maven-bom/pom.xml b/examples/maven-bom/pom.xml new file mode 100644 index 000000000..02aa07ab1 --- /dev/null +++ b/examples/maven-bom/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.example + maven-bom + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + + + org.apache.maven.plugins + maven-wrapper-plugin + 3.2.0 + + + + + + + + ru.vyarus.guicey + guicey-bom + ${dropwizard-guicey.version} + pom + import + + + + + + + ru.vyarus + dropwizard-guicey + + + + io.dropwizard + dropwizard-testing + test + + + + + \ No newline at end of file diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java new file mode 100644 index 000000000..ef44ebc4a --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java @@ -0,0 +1,31 @@ +package ru.vyarus.guice.dropwizard.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guice.dropwizard.examples.module.RootModule; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleApplication extends Application { + + public static void main(String[] args) throws Exception { + new SampleApplication().run(args); + } + + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .modules(new RootModule()) + .build()); + } + + @Override + public void run(SampleConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java new file mode 100644 index 000000000..7a8482763 --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java @@ -0,0 +1,10 @@ +package ru.vyarus.guice.dropwizard.examples; + +import io.dropwizard.core.Configuration; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleConfiguration extends Configuration { +} diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java new file mode 100644 index 000000000..ca6cd0650 --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java @@ -0,0 +1,22 @@ +package ru.vyarus.guice.dropwizard.examples.module; + +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; +import ru.vyarus.guice.dropwizard.examples.SampleConfiguration; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class RootModule extends DropwizardAwareModule { + + @Override + protected void configure() { + // 3rd party guice modules installation + install(new Some3rdPartyModule()); + + // example access to dropwizard objects from module + configuration(); + environment(); + bootstrap(); + } +} diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java new file mode 100644 index 000000000..f61fefcfe --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java @@ -0,0 +1,14 @@ +package ru.vyarus.guice.dropwizard.examples.module; + +import com.google.inject.AbstractModule; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class Some3rdPartyModule extends AbstractModule { + + @Override + protected void configure() { + } +} diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java new file mode 100644 index 000000000..22fbfe6ad --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java @@ -0,0 +1,28 @@ +package ru.vyarus.guice.dropwizard.examples.rest; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +@Path("/sample") +@Produces("application/json") +public class SampleResource { + + @Inject + private Provider requestProvider; + + @GET + @Path("/") + public Response ask() { + final String ip = requestProvider.get().getRemoteAddr(); + return Response.ok(ip).build(); + } +} diff --git a/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java new file mode 100644 index 000000000..a6ea10836 --- /dev/null +++ b/examples/maven-bom/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java @@ -0,0 +1,24 @@ +package ru.vyarus.guice.dropwizard.examples.service; + +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleService implements Managed { + + private final Logger logger = LoggerFactory.getLogger(SampleService.class); + + @Override + public void start() throws Exception { + logger.info("Starting some resource"); + } + + @Override + public void stop() throws Exception { + logger.info("Shutting down some resource"); + } +} diff --git a/examples/maven-bom/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java b/examples/maven-bom/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java new file mode 100644 index 000000000..9066652ab --- /dev/null +++ b/examples/maven-bom/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java @@ -0,0 +1,21 @@ +package ru.vyarus.guice.dropwizard.examples; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +@TestDropwizardApp(SampleApplication.class) +public class StartupTest { + + @Test + void checkStartup(ClientSupport client) { + + String res = client.targetRest("/sample").request().get().readEntity(String.class); + Assertions.assertEquals("127.0.0.1", res); + } +} diff --git a/examples/maven-settings.xml b/examples/maven-settings.xml new file mode 100644 index 000000000..be1d95817 --- /dev/null +++ b/examples/maven-settings.xml @@ -0,0 +1,27 @@ + + + + snapshots + + + + snapshots + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + + + + \ No newline at end of file diff --git a/examples/maven-simple/.mvn/wrapper/maven-wrapper.jar b/examples/maven-simple/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/examples/maven-simple/.mvn/wrapper/maven-wrapper.jar differ diff --git a/examples/maven-simple/.mvn/wrapper/maven-wrapper.properties b/examples/maven-simple/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..6d3a56651 --- /dev/null +++ b/examples/maven-simple/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/examples/maven-simple/README.md b/examples/maven-simple/README.md new file mode 100644 index 000000000..f26183bcf --- /dev/null +++ b/examples/maven-simple/README.md @@ -0,0 +1,4 @@ +# Simple maven project + +Dropwizard-guicey declared with direct dependency. +Sample junit 5 test. \ No newline at end of file diff --git a/examples/maven-simple/build.gradle b/examples/maven-simple/build.gradle new file mode 100644 index 000000000..5fe18ebd7 --- /dev/null +++ b/examples/maven-simple/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.github.dkorotych.gradle-maven-exec' +} + +// disable java compilation in gradle +tasks.withType(JavaCompile).configureEach { + enabled = false +} +tasks.withType(ProcessResources).configureEach { + enabled = false +} +tasks.withType(Jar).configureEach { + enabled = false +} + +tasks.register('maven-test', MavenExec) { + goals 'test' + options { + settings = rootProject.file('maven-settings.xml') + define=[ 'dropwizard-guicey.version':rootProject.ext.guiceyBom, 'dropwizard.version': rootProject.ext.dwVersion] + } +} + +tasks.build.dependsOn('maven-test') \ No newline at end of file diff --git a/examples/maven-simple/mvnw b/examples/maven-simple/mvnw new file mode 100755 index 000000000..8d937f4c1 --- /dev/null +++ b/examples/maven-simple/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/examples/maven-simple/mvnw.cmd b/examples/maven-simple/mvnw.cmd new file mode 100644 index 000000000..c4586b564 --- /dev/null +++ b/examples/maven-simple/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/examples/maven-simple/pom.xml b/examples/maven-simple/pom.xml new file mode 100644 index 000000000..e019ae4d1 --- /dev/null +++ b/examples/maven-simple/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.example + maven-simple + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + + + + org.apache.maven.plugins + maven-wrapper-plugin + 3.2.0 + + + + + + + ru.vyarus + dropwizard-guicey + ${dropwizard-guicey.version} + + + + io.dropwizard + dropwizard-testing + ${dropwizard.version} + test + + + + + \ No newline at end of file diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java new file mode 100644 index 000000000..ef44ebc4a --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleApplication.java @@ -0,0 +1,31 @@ +package ru.vyarus.guice.dropwizard.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.guice.dropwizard.examples.module.RootModule; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleApplication extends Application { + + public static void main(String[] args) throws Exception { + new SampleApplication().run(args); + } + + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig() + .modules(new RootModule()) + .build()); + } + + @Override + public void run(SampleConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java new file mode 100644 index 000000000..7a8482763 --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/SampleConfiguration.java @@ -0,0 +1,10 @@ +package ru.vyarus.guice.dropwizard.examples; + +import io.dropwizard.core.Configuration; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleConfiguration extends Configuration { +} diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java new file mode 100644 index 000000000..ca6cd0650 --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/RootModule.java @@ -0,0 +1,22 @@ +package ru.vyarus.guice.dropwizard.examples.module; + +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; +import ru.vyarus.guice.dropwizard.examples.SampleConfiguration; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class RootModule extends DropwizardAwareModule { + + @Override + protected void configure() { + // 3rd party guice modules installation + install(new Some3rdPartyModule()); + + // example access to dropwizard objects from module + configuration(); + environment(); + bootstrap(); + } +} diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java new file mode 100644 index 000000000..f61fefcfe --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/module/Some3rdPartyModule.java @@ -0,0 +1,14 @@ +package ru.vyarus.guice.dropwizard.examples.module; + +import com.google.inject.AbstractModule; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class Some3rdPartyModule extends AbstractModule { + + @Override + protected void configure() { + } +} diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java new file mode 100644 index 000000000..22fbfe6ad --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/rest/SampleResource.java @@ -0,0 +1,28 @@ +package ru.vyarus.guice.dropwizard.examples.rest; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +@Path("/sample") +@Produces("application/json") +public class SampleResource { + + @Inject + private Provider requestProvider; + + @GET + @Path("/") + public Response ask() { + final String ip = requestProvider.get().getRemoteAddr(); + return Response.ok(ip).build(); + } +} diff --git a/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java new file mode 100644 index 000000000..a6ea10836 --- /dev/null +++ b/examples/maven-simple/src/main/java/ru/vyarus/guice/dropwizard/examples/service/SampleService.java @@ -0,0 +1,24 @@ +package ru.vyarus.guice.dropwizard.examples.service; + +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +public class SampleService implements Managed { + + private final Logger logger = LoggerFactory.getLogger(SampleService.class); + + @Override + public void start() throws Exception { + logger.info("Starting some resource"); + } + + @Override + public void stop() throws Exception { + logger.info("Shutting down some resource"); + } +} diff --git a/examples/maven-simple/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java b/examples/maven-simple/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java new file mode 100644 index 000000000..9066652ab --- /dev/null +++ b/examples/maven-simple/src/test/java/ru/vyarus/guice/dropwizard/examples/StartupTest.java @@ -0,0 +1,21 @@ +package ru.vyarus.guice.dropwizard.examples; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; + +/** + * @author Vyacheslav Rusakov + * @since 03.07.2023 + */ +@TestDropwizardApp(SampleApplication.class) +public class StartupTest { + + @Test + void checkStartup(ClientSupport client) { + + String res = client.targetRest("/sample").request().get().readEntity(String.class); + Assertions.assertEquals("127.0.0.1", res); + } +} diff --git a/examples/openapi-client-server/README.md b/examples/openapi-client-server/README.md new file mode 100644 index 000000000..cf87ea3bb --- /dev/null +++ b/examples/openapi-client-server/README.md @@ -0,0 +1,187 @@ +# OpenAPI 3 client and server examples + +Source: https://petstore3.swagger.io/ + +Example show client and fake server generation from openapi declaration (json or yaml). + +NOTE: example uses [gradle plugin](https://openapi-generator.tech/docs/integrations#gradle-integration), +but [maven plugin](https://openapi-generator.tech/docs/integrations#maven-integration) is also available + +Build project to generate client and server (openapi generation tasks called implicitly). + +`openApiGenerate` creates client in build/petstore/client +`openApiGenerateServer` creates server stub in build/petstore/server + +(Refresh IDEA project to attach generated sources) + +## General + +Important dependencies: + +```groovy +dependencies { + implementation 'io.dropwizard:dropwizard-forms' + implementation 'com.github.scribejava:scribejava-core:8.3.3' + + // OPENAPI CODEGEN + // additional dependency required for codegen, but conflicts with dropwizard (need to disable feature) + implementation 'org.glassfish.jersey.media:jersey-media-json-jackson' + + compileOnly 'io.swagger.core.v3:swagger-annotations:2.2.30' +} +``` + +## Client + +Actual client interfaces are generated in: + +``` +/build/petstore/client/src/main/java/com/petstore/api +``` + +These are the main client classes: + +* `PetApi` +* `StoreApi` +* `UserApi` + +Guice bindings: + +```java +public class PetStoreApiModule extends DropwizardAwareModule { + + @Override + protected void configure() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(configuration().getPetStoreUrl()); + // optional + apiClient.setDebugging(true); + + bind(ApiClient.class).toInstance(apiClient); + bind(PetApi.class).toInstance(new PetApi(apiClient)); + bind(StoreApi.class).toInstance(new StoreApi(apiClient)); + bind(UserApi.class).toInstance(new UserApi(apiClient)); + } +} +``` + +(target url is in configuration) + + +## Server + +Generated server stub: + +``` +/build/petstore/server/src/main/java/com/petstore/server +``` + +NOTE: server will also generate its own model, but this part will be attached directly (from `gen` folder). + +Now copy server files into the main sources (preserving package): + +``` +/build/petstore/server/src/main/java/com/petstore/server --> /src/main/java/com/petstore/server +``` + +(except `Bootstrap` class) + +Implement fakes in `impl`. For example, to implement getPetById, change `PetApiServiceImpl` : + +```java +@Override +public Response getPetById(Long petId, SecurityContext securityContext) throws NotFoundException { + final Pet pet = new Pet(); + pet.setId(petId); + pet.setName("Jack"); + final Tag tag = new Tag(); + tag.setName("puppy"); + pet.getTags().add(tag); + return Response.ok().entity(pet).build(); +} +``` + +Now implement root fake resource: + +```java +@Path("/fake/petstore/") +public class FakePetStoreServer { + + // IMPORTANT: paths in console would contain /pet/pet duplicate, but ACTUAL path matching would IGNORE + // @Path("/pet") declared on ApiApi class, so such declaration is correct for runtime + + @Path("/pet") + public Class getPetApi() { + return PetApi.class; + } + + @Path("/store") + public Class getStoreApi() { + return StoreApi.class; + } + + @Path("/user") + public Class getUserApi() { + return UserApi.class; + } +} +``` + +## Bundle + +Use GuiceyBundle for activation: + +```java +public class PetStoreBundle implements GuiceyBundle { + + @Override + public void run(GuiceyEnvironment environment) throws Exception { + // because of required conflicting dependency jersey-media-json-jackson + // https://github.com/dropwizard/dropwizard/issues/1341#issuecomment-251503011 + environment.environment().jersey() + .property(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, Boolean.TRUE); + + // register client api + environment.modules(new PetStoreApiModule()); + + // optional fake server start + if (environment.configuration().isStartFakeStore()) { + environment.register(FakePetStoreServer.class); + } + } +} +``` + +Registration in app: + +```java +@Override +public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new PetStoreBundle()) + .build()); +} +``` + +## Usage + +```java +@TestDropwizardApp(value = ExampleApp.class, + configOverride = { + "petStoreUrl: http://localhost:8080/fake/petstore", + "startFakeStore: true"}) +public class FakeServerTest { + + @Inject + SampleService sampleService; + + @Test + void testServer() { + + final Pet pet = sampleService.findPet(1); + Assertions.assertNotNull(pet); + Assertions.assertEquals("Jack", pet.getName()); + } +} +``` + diff --git a/examples/openapi-client-server/build.gradle b/examples/openapi-client-server/build.gradle new file mode 100644 index 000000000..b9d4a67f8 --- /dev/null +++ b/examples/openapi-client-server/build.gradle @@ -0,0 +1,60 @@ +import org.openapitools.generator.gradle.plugin.tasks.GenerateTask + +plugins { + id 'org.openapi.generator' +} + +java { + withSourcesJar() +} + +dependencies { + implementation 'io.dropwizard:dropwizard-forms' + implementation 'com.github.scribejava:scribejava-core:8.3.3' + + // OPENAPI CODEGEN + // additional dependency required for codegen, but conflicts with dropwizard (need to disable feature) + implementation 'org.glassfish.jersey.media:jersey-media-json-jackson' + + compileOnly 'io.swagger.core.v3:swagger-annotations:2.2.30' +} + +// https://openapi-generator.tech/docs/generators/java +openApiGenerate { + generatorName = "java" + inputSpec = "$projectDir/src/main/openapi/petStore.yaml" + outputDir = "$buildDir/petstore/client" + apiPackage = "com.petstore.api" + invokerPackage = "com.petstore" + modelPackage = "com.petstore.api.model" + configOptions = [ + library: "jersey3", + dateLibrary: "java8", + openApiNullable: "false", + hideGenerationTimestamp: "true" + ] +} + + +// https://openapi-generator.tech/docs/generators/jaxrs-jersey +tasks.register('openApiGenerateServer', GenerateTask) { + group = 'openapi tools' + generatorName = "jaxrs-jersey" + inputSpec = "$projectDir/src/main/openapi/petStore.yaml" + outputDir = "$buildDir/petstore/server" + apiPackage = "com.petstore.server.api" + invokerPackage = "com.petstore.server" + modelPackage = "com.petstore.server.api.model" + configOptions = [ + library : "jersey3", + dateLibrary: "java8", + openApiNullable: "false", + hideGenerationTimestamp: "true" + ] +} + +compileJava.dependsOn 'openApiGenerate', 'openApiGenerateServer' +tasks.sourcesJar.dependsOn 'openApiGenerate', 'openApiGenerateServer' +sourceSets.main.java.srcDir "${openApiGenerate.outputDir.get()}/src/main/java" +// note main folder not attached! (sources were copied manually) +sourceSets.main.java.srcDir "${openApiGenerateServer.outputDir.get()}/src/gen/java" diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/PetApiServiceFactory.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/PetApiServiceFactory.java new file mode 100644 index 000000000..244e2817a --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/PetApiServiceFactory.java @@ -0,0 +1,13 @@ +package com.petstore.server.api.factories; + +import com.petstore.server.api.PetApiService; +import com.petstore.server.api.impl.PetApiServiceImpl; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class PetApiServiceFactory { + private static final PetApiService service = new PetApiServiceImpl(); + + public static PetApiService getPetApi() { + return service; + } +} diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/StoreApiServiceFactory.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/StoreApiServiceFactory.java new file mode 100644 index 000000000..f215efd3f --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/StoreApiServiceFactory.java @@ -0,0 +1,14 @@ +package com.petstore.server.api.factories; + +import com.petstore.server.api.StoreApiService; +import com.petstore.server.api.impl.StoreApiServiceImpl; + + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class StoreApiServiceFactory { + private static final StoreApiService service = new StoreApiServiceImpl(); + + public static StoreApiService getStoreApi() { + return service; + } +} diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/UserApiServiceFactory.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/UserApiServiceFactory.java new file mode 100644 index 000000000..ec420a0a0 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/factories/UserApiServiceFactory.java @@ -0,0 +1,13 @@ +package com.petstore.server.api.factories; + +import com.petstore.server.api.UserApiService; +import com.petstore.server.api.impl.UserApiServiceImpl; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class UserApiServiceFactory { + private static final UserApiService service = new UserApiServiceImpl(); + + public static UserApiService getUserApi() { + return service; + } +} diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/PetApiServiceImpl.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/PetApiServiceImpl.java new file mode 100644 index 000000000..c743ebef4 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/PetApiServiceImpl.java @@ -0,0 +1,70 @@ +package com.petstore.server.api.impl; + +import com.petstore.server.api.*; +import java.io.File; +import com.petstore.server.api.model.ModelApiResponse; +import com.petstore.server.api.model.Pet; + +import java.util.Arrays; +import java.util.List; +import com.petstore.server.api.NotFoundException; + +import java.io.InputStream; + +import com.petstore.server.api.model.Tag; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class PetApiServiceImpl extends PetApiService { + @Override + public Response addPet(Pet pet, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response deletePet(Long petId, String apiKey, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response findPetsByStatus(String status, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response findPetsByTags(List tags, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + + @Override + public Response getPetById(Long petId, SecurityContext securityContext) throws NotFoundException { + final Pet pet = new Pet(); + pet.setId(petId); + pet.setName("Jack"); + final Tag tag = new Tag(); + tag.setName("puppy"); + pet.getTags().add(tag); + return Response.ok().entity(pet).build(); + } + + @Override + public Response updatePet(Pet pet, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response updatePetWithForm(Long petId, String name, String status, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response uploadFile(Long petId, String additionalMetadata, File body, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } +} diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/StoreApiServiceImpl.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/StoreApiServiceImpl.java new file mode 100644 index 000000000..a3902c6a3 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/StoreApiServiceImpl.java @@ -0,0 +1,40 @@ +package com.petstore.server.api.impl; + +import com.petstore.server.api.*; +import java.util.Map; +import com.petstore.server.api.model.Order; + +import java.util.List; +import com.petstore.server.api.NotFoundException; + +import java.io.InputStream; + +import org.glassfish.jersey.media.multipart.FormDataBodyPart; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class StoreApiServiceImpl extends StoreApiService { + @Override + public Response deleteOrder(Long orderId, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response getInventory(SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response getOrderById(Long orderId, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response placeOrder(Order order, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } +} diff --git a/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/UserApiServiceImpl.java b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/UserApiServiceImpl.java new file mode 100644 index 000000000..2d5b5b559 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/com/petstore/server/api/impl/UserApiServiceImpl.java @@ -0,0 +1,55 @@ +package com.petstore.server.api.impl; + +import com.petstore.server.api.*; +import java.time.OffsetDateTime; +import com.petstore.server.api.model.User; + +import java.util.List; +import com.petstore.server.api.NotFoundException; + +import java.io.InputStream; + +import org.glassfish.jersey.media.multipart.FormDataBodyPart; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJerseyServerCodegen", comments = "Generator version: 7.12.0") +public class UserApiServiceImpl extends UserApiService { + @Override + public Response createUser(User user, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response createUsersWithListInput(List<@Valid User> user, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response deleteUser(String username, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response getUserByName(String username, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response loginUser(String username, String password, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response logoutUser(SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } + @Override + public Response updateUser(String username, User user, SecurityContext securityContext) throws NotFoundException { + // do some magic! + return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleApp.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleApp.java new file mode 100644 index 000000000..3aac7f58e --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleApp.java @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Application; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.examples.petstore.PetStoreApiModule; +import ru.vyarus.dropwizard.guice.examples.petstore.PetStoreBundle; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +public class ExampleApp extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new PetStoreBundle()) + .build()); + } + + @Override + public void run(ExampleConfig config, Environment environment) throws Exception { + + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleConfig.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleConfig.java new file mode 100644 index 000000000..495f54bb1 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/ExampleConfig.java @@ -0,0 +1,21 @@ +package ru.vyarus.dropwizard.guice.examples; + +import io.dropwizard.core.Configuration; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +public class ExampleConfig extends Configuration { + + private String petStoreUrl; + private boolean startFakeStore; + + public String getPetStoreUrl() { + return petStoreUrl; + } + + public boolean isStartFakeStore() { + return startFakeStore; + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/FakePetStoreServer.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/FakePetStoreServer.java new file mode 100644 index 000000000..0ecc0fc42 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/FakePetStoreServer.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.examples.petstore; + +import com.petstore.server.api.PetApi; +import com.petstore.server.api.StoreApi; +import com.petstore.server.api.UserApi; +import jakarta.ws.rs.Path; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +@Path("/fake/petstore/") +public class FakePetStoreServer { + + // IMPORTANT: paths in console would contain /pet/pet duplicate, but ACTUAL path matching would IGNORE + // @Path("/pet") declared on ApiApi class, so such declaration is correct for runtime + + @Path("/pet") + public Class getPetApi() { + return PetApi.class; + } + + @Path("/store") + public Class getStoreApi() { + return StoreApi.class; + } + + @Path("/user") + public Class getUserApi() { + return UserApi.class; + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreApiModule.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreApiModule.java new file mode 100644 index 000000000..3cb61d4da --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreApiModule.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.examples.petstore; + +import com.petstore.ApiClient; +import com.petstore.api.PetApi; +import com.petstore.api.StoreApi; +import com.petstore.api.UserApi; +import ru.vyarus.dropwizard.guice.examples.ExampleConfig; +import ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +public class PetStoreApiModule extends DropwizardAwareModule { + + @Override + protected void configure() { + ApiClient apiClient = new ApiClient(); + apiClient.setBasePath(configuration().getPetStoreUrl()); + // optional + apiClient.setDebugging(true); + + bind(ApiClient.class).toInstance(apiClient); + bind(PetApi.class).toInstance(new PetApi(apiClient)); + bind(StoreApi.class).toInstance(new StoreApi(apiClient)); + bind(UserApi.class).toInstance(new UserApi(apiClient)); + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreBundle.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreBundle.java new file mode 100644 index 000000000..af70c7d22 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/petstore/PetStoreBundle.java @@ -0,0 +1,29 @@ +package ru.vyarus.dropwizard.guice.examples.petstore; + +import org.glassfish.jersey.CommonProperties; +import ru.vyarus.dropwizard.guice.examples.ExampleConfig; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +public class PetStoreBundle implements GuiceyBundle { + + @Override + public void run(GuiceyEnvironment environment) throws Exception { + // because of required conflicting dependency jersey-media-json-jackson + // https://github.com/dropwizard/dropwizard/issues/1341#issuecomment-251503011 + environment.environment().jersey() + .property(CommonProperties.FEATURE_AUTO_DISCOVERY_DISABLE, Boolean.TRUE); + + // register client api + environment.modules(new PetStoreApiModule()); + + // optional fake server start + if (environment.configuration().isStartFakeStore()) { + environment.register(FakePetStoreServer.class); + } + } +} diff --git a/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java new file mode 100644 index 000000000..78c086d62 --- /dev/null +++ b/examples/openapi-client-server/src/main/java/ru/vyarus/dropwizard/guice/examples/service/SampleService.java @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.examples.service; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.petstore.ApiException; +import com.petstore.api.PetApi; +import com.petstore.api.model.Pet; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +@Singleton +public class SampleService { + + @Inject + PetApi petApi; + + public Pet findPet(long id) { + try { + return petApi.getPetById(id); + } catch (ApiException e) { + throw new IllegalStateException("Failed to call petclinic", e); + } + } +} diff --git a/examples/openapi-client-server/src/main/openapi/petStore.yaml b/examples/openapi-client-server/src/main/openapi/petStore.yaml new file mode 100644 index 000000000..e7c754879 --- /dev/null +++ b/examples/openapi-client-server/src/main/openapi/petStore.yaml @@ -0,0 +1,839 @@ +openapi: 3.0.4 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: https://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.27-SNAPSHOT +externalDocs: + description: Find out more about Swagger + url: https://swagger.io +servers: + - url: https://petstore3.swagger.io/api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: https://swagger.io + - name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: https://swagger.io + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet. + description: Update an existing pet by Id. + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '422': + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store. + description: Add a new pet to the store. + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid input + '422': + description: Validation exception + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status. + description: Multiple status values can be provided with comma separated strings. + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags. + description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID. + description: Returns a single pet. + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + default: + description: Unexpected error + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data. + description: Updates a pet resource based on the form data. + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid input + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet. + description: Delete a pet. + operationId: deletePet + parameters: + - name: api_key + in: header + description: '' + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: Pet deleted + '400': + description: Invalid pet value + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image. + description: Upload image of the pet. + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '400': + description: No file uploaded + '404': + description: Pet not found + default: + description: Unexpected error + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status. + description: Returns a map of status codes to quantities. + operationId: getInventory + x-swagger-router-controller: OrderController + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + default: + description: Unexpected error + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet. + description: Place a new order in the store. + x-swagger-router-controller: OrderController + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid input + '422': + description: Validation exception + default: + description: Unexpected error + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID. + x-swagger-router-controller: OrderController + description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + default: + description: Unexpected error + delete: + tags: + - store + summary: Delete purchase order by identifier. + description: For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. + x-swagger-router-controller: OrderController + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: order deleted + '400': + description: Invalid ID supplied + '404': + description: Order not found + default: + description: Unexpected error + /user: + post: + tags: + - user + summary: Create user. + description: This can only be done by the logged in user. + x-swagger-router-controller: UserController + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array. + description: Creates list of users with given input array. + x-swagger-router-controller: UserController + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: Unexpected error + /user/login: + get: + tags: + - user + summary: Logs user into the system. + description: Log into the system. + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + default: + description: Unexpected error + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session. + description: Log user out of the system. + operationId: logoutUser + parameters: [] + responses: + '200': + description: successful operation + default: + description: Unexpected error + /user/{username}: + get: + tags: + - user + summary: Get user by user name. + description: Get user detail based on username. + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + default: + description: Unexpected error + put: + tags: + - user + summary: Update user resource. + description: This can only be done by the logged in user. + x-swagger-router-controller: UserController + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: successful operation + '400': + description: bad request + '404': + description: user not found + default: + description: Unexpected error + delete: + tags: + - user + summary: Delete user resource. + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '200': + description: User deleted + '400': + description: Invalid username supplied + '404': + description: User not found + default: + description: Unexpected error +components: + schemas: + Order: + x-swagger-router-model: io.swagger.petstore.model.Order + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Category: + x-swagger-router-model: io.swagger.petstore.model.Category + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + x-swagger-router-model: io.swagger.petstore.model.User + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: '12345' + phone: + type: string + example: '12345' + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + x-swagger-router-model: io.swagger.petstore.model.Tag + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + x-swagger-router-model: io.swagger.petstore.model.Pet + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + type: apiKey + name: api_key + in: header \ No newline at end of file diff --git a/examples/openapi-client-server/src/test/java/ru/vyarus/dropwizard/guice/example/FakeServerTest.java b/examples/openapi-client-server/src/test/java/ru/vyarus/dropwizard/guice/example/FakeServerTest.java new file mode 100644 index 000000000..90312bce3 --- /dev/null +++ b/examples/openapi-client-server/src/test/java/ru/vyarus/dropwizard/guice/example/FakeServerTest.java @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.example; + +import com.google.inject.Inject; +import com.petstore.api.model.Pet; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import ru.vyarus.dropwizard.guice.examples.ExampleApp; +import ru.vyarus.dropwizard.guice.examples.service.SampleService; +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp; +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; + +/** + * @author Vyacheslav Rusakov + * @since 07.05.2025 + */ +@TestDropwizardApp(value = ExampleApp.class, + configOverride = { + "petStoreUrl: http://localhost:8080/fake/petstore", + "startFakeStore: true"}) +public class FakeServerTest { + + @Inject + SampleService sampleService; + + @Test + void testServer() { + + final Pet pet = sampleService.findPet(1); + Assertions.assertNotNull(pet); + Assertions.assertEquals("Jack", pet.getName()); + } +} diff --git a/examples/settings.gradle b/examples/settings.gradle new file mode 100644 index 000000000..c39e36551 --- /dev/null +++ b/examples/settings.gradle @@ -0,0 +1,21 @@ +rootProject.name = 'dropwizard-guicey-examples' + +include 'core-getting-started', + 'core-extensions', + 'core-servlets', + 'core-rest-sub-resource', + 'core-bundle-plug-n-play', + 'core-installers-reset', + 'core-installer-custom', + 'ext-jdbi3', + 'ext-eventbus', + 'ext-spa', + 'ext-gsp', + 'ext-gsp-spa', + 'integration-auth', + 'integration-hibernate', + 'integration-guice-validator', + 'integration-dropwizard-jobs', + 'maven-bom', + 'maven-simple', + 'openapi-client-server' \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index cf9dd85b4..e51842f4b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.6.1-SNAPSHOT +version=8.0.1-SNAPSHOT diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4..d64cd4917 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb8790..0aaefbcaf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..25da30dbd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/guicey-admin-rest/README.md b/guicey-admin-rest/README.md new file mode 100644 index 000000000..7ee61aa2a --- /dev/null +++ b/guicey-admin-rest/README.md @@ -0,0 +1,98 @@ +# Admin REST + +Mirror all resources in admin context: on admin side special servlet simply redirects all incoming requests into the jersey context. +Hides admin-only resources from user context: resource is working under admin context and return 404 on user context. + +Such approach is better than registering a completely separate jersey context for admin rest because +of no overhead and the simplicity of jersey extensions management. + +Features: +* All user context rest available in admin context +* Admin-only resources not visible in user context + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-admin-rest + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-admin-rest:{guicey.version}' +``` + +Omit version if guicey BOM used. + + +### Usage + +Register bundle: + +```java +GuiceBundle.builder() + .bundles(new AdminRestBundle()); +``` + +In this case, rest is registered either to '/api/*', if main context rest is mapped to root ('/*') +or to the same path as main context rest. + +To register on a custom path: + +```java +.bundles(new AdminRestBundle("/custom/*")); +``` + +#### Security + +In order to hide specific resource methods or entire resources on the main context, annotate resource methods +or resource classes with the `@AdminResource` annotation. + +For example: + +```java +@GET +@Path("/admin") +@AdminResource +public String admin() { + return "admin" +} +``` + +This (annotated) method will return 404 error when called from main context, but will function normally +when called from the admin context. + +#### Logs + +As admin servlet just redirects to the main context, then all admin rest requests would be logged like this + +``` +127.0.0.1 - - [17/Sep/2024:09:27:43 +0000] "GET /async HTTP/1.1" 200 5 "-" "Java/17.0.2" 342 +``` + +If custom mapping path is used then admin requests could be identified easily: + +``` +127.0.0.1 - - [17/Sep/2024:09:27:43 +0000] "GET /api/async HTTP/1.1" 200 5 "-" "Java/17.0.2" 202 +``` + +(here "/async" path showed under "/api" context, whereas in the main context it is mapped on root) + +If you need an additional identification, then enable it with `identifyAdminContextInRequestLogs`: + +```java +GuiceBundle.builder() + .bundles(new AdminRestBundle().identifyAdminContextInRequestLogs()); +``` + +With it all admin calls would have " (ADMIN REST)" identity appended to uri: + +``` +127.0.0.1 - - [17/Sep/2024:09:32:16 +0000] "GET /api/async (ADMIN REST) HTTP/1.1" 200 5 "-" "Java/17.0.2" 202 +``` diff --git a/guicey-admin-rest/build.gradle b/guicey-admin-rest/build.gradle new file mode 100644 index 000000000..47496c04b --- /dev/null +++ b/guicey-admin-rest/build.gradle @@ -0,0 +1,5 @@ +description = "Mirror rest in admin context" + +dependencies { + testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8' +} \ No newline at end of file diff --git a/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/AdminRestBundle.java b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/AdminRestBundle.java new file mode 100644 index 000000000..4999dc9ed --- /dev/null +++ b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/AdminRestBundle.java @@ -0,0 +1,106 @@ +package ru.vyarus.guicey.admin; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.guicey.admin.log.LogbackAccessRequestLogAwareCustomHandler; +import ru.vyarus.guicey.admin.rest.AdminResourceFilter; +import ru.vyarus.guicey.admin.rest.AdminRestServlet; + +/** + * Adds rest support in admin context by simply redirecting from admin servlet into jersey (all rest methods + * are accessible from admin context). + *

        + * Such approach is better than registering completely separate jersey context for admin rest because + * of no overhead and simplicity of jersey extensions management. + *

        + * If no specific mapping path specified, rest will be mapped into the same path as main rest. + * If main rest mapping default ('/*') isn't changed, admin rest will be mapped to '/api/*'. + *

        + * In order to hide admin specific rest methods or entire resources + * {@link ru.vyarus.guicey.admin.rest.AdminResource} annotation may be used. + * If some security solution is used within application, rest could be hidden with security framework permissions. + * + * @author Vyacheslav Rusakov + * @since 05.08.2015 + */ +public class AdminRestBundle extends UniqueGuiceyBundle { + private static final String ROOT_PATH = "/*"; + private final Logger logger = LoggerFactory.getLogger(AdminRestBundle.class); + + private final String path; + + /** + * Admin rest will be mapped on the same path as main rest if rest mapping is different from '/*'. + * Otherwise admin rest mapped to '/api/*'. + */ + public AdminRestBundle() { + this(null); + } + + /** + * Path must end with '/*', otherwise error will be thrown. + * For example, '/rest/*' is a valid path. + * + * @param path path to map admin rest on + */ + public AdminRestBundle(final String path) { + this.path = path; + } + + @Override + public void run(final GuiceyEnvironment environment) throws Exception { + environment.manage(new ServletRegistration(environment.environment())); + } + + private void registerServlet(final String path, final Environment environment) { + environment.admin() + .addServlet("adminRest", new AdminRestServlet(environment.getJerseyServletContainer())) + .addMapping(path); + environment.jersey().register(AdminResourceFilter.class); + // dropwizard request logging consists of two parts: LogbackAccessRequestLogAwareHandler + // prepares request for logging and LogbackAccessRequestLog performs log + // In admin context LogbackAccessRequestLogAwareHandler not registered, but our admin servlet calls + // the main context, which will trigger LogbackAccessRequestLog, but without a proper handler it would fail + environment.getAdminContext() + .insertHandler(new LogbackAccessRequestLogAwareCustomHandler()); + logger.info("Admin REST registered on path: {}", path); + } + + /** + * Managed object is required because rest mapping from configuration (servlet.rootPath) + * is available only on managed start phase. + */ + private class ServletRegistration implements Managed { + private final Environment environment; + + ServletRegistration(final Environment environment) { + this.environment = environment; + } + + @Override + public void start() throws Exception { + String restPath = path; + if (Strings.isNullOrEmpty(restPath)) { + restPath = environment.jersey().getUrlPattern(); + // we can't map rest to the root of admin context + if (ROOT_PATH.equals(restPath)) { + restPath = "/api/*"; + } + } + Preconditions.checkState(restPath.endsWith(ROOT_PATH), + "Rest path must end with '/*', but configured one is not: %s", restPath); + registerServlet(restPath, environment); + } + + @Override + public void stop() throws Exception { + // not needed + } + } +} diff --git a/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/log/LogbackAccessRequestLogAwareCustomHandler.java b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/log/LogbackAccessRequestLogAwareCustomHandler.java new file mode 100644 index 000000000..6b9b3e273 --- /dev/null +++ b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/log/LogbackAccessRequestLogAwareCustomHandler.java @@ -0,0 +1,47 @@ +package ru.vyarus.guicey.admin.log; + +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.internal.HttpChannelState; +import org.eclipse.jetty.util.Callback; +import ru.vyarus.guicey.admin.rest.AdminRestServlet; + +/** + * This is almost a copy of {@link io.dropwizard.request.logging.LogbackAccessRequestLogAwareHandler} registered + * in the main context. This handler is required to prepare a correct request instance for logger + * (see {@code channelRequest.setLoggedRequest} line). Without handler, + * {@link io.dropwizard.request.logging.LogbackAccessRequestLog} will fail with error. + *

        + * In admin context, request log is not registered, but admin servlet calls main context and so invokes + * request logger. To avoid exceptions, we need to prepare logger request inside admin context, but only for + * rest emulation servlet. + *

        + * Pay attention: admin rest requests are logged the same way as usual rest requests! + * + * @author Vyacheslav Rusakov + * @since 17.09.2024 + */ +public class LogbackAccessRequestLogAwareCustomHandler extends Handler.Wrapper { + + @Override + public boolean handle(final Request request, + final Response response, + final Callback callback) throws Exception { + final boolean handled = super.handle(request, response, callback); + // apply ONLY for rest simulation (for other cases simply not required, because requests not logged) + if (handled && request.getAttribute(AdminRestServlet.ADMIN_PROPERTY) != null) { + final ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + if (servletContextRequest != null) { + final Request unwrapped = Request.unWrap(request); + if (!(unwrapped instanceof HttpChannelState.ChannelRequest channelRequest)) { + throw new IllegalStateException( + "Expecting unwrapped request to be an instance of HttpChannelState.ChannelRequest"); + } + channelRequest.setLoggedRequest(servletContextRequest); + } + } + return handled; + } +} diff --git a/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResource.java b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResource.java new file mode 100644 index 000000000..355754749 --- /dev/null +++ b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResource.java @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.admin.rest; + +import jakarta.ws.rs.NameBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Security annotation to deny access to admin specific rest from user context. + * Annotation may be used on resource class to hide all resource methods or directly + * on methods (for hybrid cases). + *

        + * When secured resource is accessed from user context, 403 error will be returned. + *

        + * Requires {@link ru.vyarus.guicey.admin.AdminRestBundle} to be registered, otherwise will not + * have any effect. + * + * @author Vyacheslav Rusakov + * @since 04.08.2015 + */ +@NameBinding +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AdminResource { +} diff --git a/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResourceFilter.java b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResourceFilter.java new file mode 100644 index 000000000..8b2bb0579 --- /dev/null +++ b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminResourceFilter.java @@ -0,0 +1,25 @@ +package ru.vyarus.guicey.admin.rest; + +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import java.io.IOException; + +/** + * {@link AdminResource} annotation support. Denies rest method processing if accessed not from admin context. + * + * @author Vyacheslav Rusakov + * @since 04.08.2015 + */ +@AdminResource +public class AdminResourceFilter implements ContainerRequestFilter { + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + final Boolean isAdmin = (Boolean) requestContext.getProperty(AdminRestServlet.ADMIN_PROPERTY); + if (isAdmin == null || !isAdmin) { + // 404 - resource not exists for outer world + throw new NotFoundException(); + } + } +} diff --git a/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminRestServlet.java b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminRestServlet.java new file mode 100644 index 000000000..a1d9b5911 --- /dev/null +++ b/guicey-admin-rest/src/main/java/ru/vyarus/guicey/admin/rest/AdminRestServlet.java @@ -0,0 +1,45 @@ +package ru.vyarus.guicey.admin.rest; + +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * Forwards all requests into jersey context. + *

        + * Marks request with custom attribute {@link #ADMIN_PROPERTY} to indicate admin rest usage. + * It may be used later to recognize rest origin. For example, {@link AdminResourceFilter} use it to prevent + * access to admin resources (annotated with {@link AdminResource}) from user context. + * + * @author Vyacheslav Rusakov + * @since 04.08.2015 + */ +public class AdminRestServlet extends HttpServlet { + /** + * Request attribute name set with 'true' value to distinguish admin rest from user context rest call. + */ + public static final String ADMIN_PROPERTY = AdminRestServlet.class.getName(); + + /** + * Servlet context. + */ + private final Servlet restServlet; + + /** + * @param restServlet dropwizard rest servlet (environment.getJerseyServletContainer()) + */ + public AdminRestServlet(final Servlet restServlet) { + this.restServlet = restServlet; + } + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + req.setAttribute(ADMIN_PROPERTY, true); + restServlet.service(req, resp); + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AbstractTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AbstractTest.groovy new file mode 100644 index 000000000..5baad4775 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AbstractTest.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.admin + +import ch.qos.logback.classic.Level +import io.dropwizard.logging.common.BootstrapLogging +import io.dropwizard.logging.common.LoggingUtil +import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 22.10.2019 + */ +class AbstractTest extends Specification { + static { + BootstrapLogging.bootstrap(Level.DEBUG); // bootstrap set threshold filter! + LoggingUtil.getLoggerContext().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).setLevel(Level.WARN); + LoggingUtil.getLoggerContext().getLogger("ru.vyarus.dropwizard.guice").setLevel(Level.INFO); + } + + void cleanupSpec() { + // some tests are intentionally failing so be sure to remove stale applications + SharedConfigurationState.clear() + System.clearProperty(PropertyBundleLookup.BUNDLES_PROPERTY) + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminBundleTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminBundleTest.groovy new file mode 100644 index 000000000..9e8d60c86 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminBundleTest.groovy @@ -0,0 +1,49 @@ +package ru.vyarus.guicey.admin + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.admin.support.AdminRestApplication + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +@TestDropwizardApp(AdminRestApplication) +class AdminBundleTest extends AbstractTest { + + def "Check res access from user context"() { + + when: "opened rest" + def res = new URL("http://localhost:8080/hybrid/hello").getText() + then: "ok" + res == "hello" + + when: "admin only rest" + new URL("http://localhost:8080/hybrid/admin").getText() + then: "not accessible" + thrown(FileNotFoundException) + + when: "admin only rest (by class)" + new URL("http://localhost:8080/admin/").getText() + then: "not accessible" + thrown(FileNotFoundException) + } + + def "Check access from admin context"() { + + // when rest is registered to root, admin rest is accessible from /api/ + when: "public rest" + def res = new URL("http://localhost:8081/api/hybrid/hello").getText() + then: "ok" + res == "hello" + + when: "admin rest" + res = new URL("http://localhost:8081/api/hybrid/admin").getText() + then: "ok" + res == "admin" + + when: "admin rest (by class annotation)" + res = new URL("http://localhost:8081/api/admin/").getText() + then: "ok" + res == "hello" + } +} \ No newline at end of file diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminRestIdentityInLogsTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminRestIdentityInLogsTest.groovy new file mode 100644 index 000000000..b854971cf --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AdminRestIdentityInLogsTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.admin + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.junit.jupiter.api.extension.ExtendWith +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.admin.support.HybridResource +import uk.org.webcompere.systemstubs.jupiter.SystemStub +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension +import uk.org.webcompere.systemstubs.stream.SystemOut + +/** + * @author Vyacheslav Rusakov + * @since 17.09.2024 + */ +@TestDropwizardApp(App) +@ExtendWith(SystemStubsExtension) +class AdminRestIdentityInLogsTest extends AbstractTest { + + @SystemStub + SystemOut out + + def "Check resource logs differ for admin context"() { + + when: "opened rest" + def res = new URL("http://localhost:8080/hybrid/hello").getText() + sleep(100) + then: "ok" + out.getText().replace("\r", "").contains("\"GET /hybrid/hello HTTP/1.1\"") + + when: "admin only rest" + res = new URL("http://localhost:8081/api/hybrid/hello").getText() + sleep(100) + then: "admin context identified" + out.getText().replace("\r", "").contains("\"GET /api/hybrid/hello HTTP/1.1\"") + + } + + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new AdminRestBundle()) + .extensions(HybridResource) + .build() + ) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AsyncResourceTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AsyncResourceTest.groovy new file mode 100644 index 000000000..e10074294 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/AsyncResourceTest.groovy @@ -0,0 +1,77 @@ +package ru.vyarus.guicey.admin + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.glassfish.jersey.server.ManagedAsync +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import spock.lang.Specification + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended +import java.util.concurrent.CompletableFuture + +/** + * @author Vyacheslav Rusakov + * @since 18.08.2016 + */ +@TestDropwizardApp(AsyncRestApp) +// todo FIXME - problem!! LOOK!! +class AsyncResourceTest extends Specification { + + def "Check async resource"() { + + expect: "resource works" + new URL("http://localhost:8080/async").getText() == 'done!' + new URL("http://localhost:8080/async/managed").getText() == 'done managed!' + + and: "admin rest works" + new URL("http://localhost:8081/api/async").getText() == 'done!' + new URL("http://localhost:8081/api/async/managed").getText() == 'done managed!' + } + + static class AsyncRestApp extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new AdminRestBundle("/api/*")) + .extensions(AsyncResource) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + + } + } + + @Path("/async") + static class AsyncResource { + + @GET + public void asyncGet(@Suspended final AsyncResponse asyncResponse) { + String thread = Thread.currentThread().name + CompletableFuture + .runAsync({ + assert thread != Thread.currentThread().name + println "expensive async task" + sleep(200) + }) + .thenApply({ result -> asyncResponse.resume("done!") }); + } + + @GET + @ManagedAsync + @Path("/managed") + public void managedAsyncGet(@Suspended final AsyncResponse asyncResponse) { + println "expensive managed async task"; + sleep(200) + asyncResponse.resume("done managed!") + } + } +} \ No newline at end of file diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/ManualAdminRestMappingTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/ManualAdminRestMappingTest.groovy new file mode 100644 index 000000000..65affe241 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/ManualAdminRestMappingTest.groovy @@ -0,0 +1,30 @@ +package ru.vyarus.guicey.admin + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.admin.support.ManualAdminRestPathApp + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +@TestDropwizardApp(ManualAdminRestPathApp) +class ManualAdminRestMappingTest extends AbstractTest { + + def "Check access from admin context"() { + + when: "public rest" + def res = new URL("http://localhost:8081/rest/hybrid/hello").getText() + then: "ok" + res == "hello" + + when: "admin rest" + res = new URL("http://localhost:8081/rest/hybrid/admin").getText() + then: "ok" + res == "admin" + + when: "admin rest (by class annotation)" + res = new URL("http://localhost:8081/rest/admin/").getText() + then: "ok" + res == "hello" + } +} \ No newline at end of file diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/RequestScopeTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/RequestScopeTest.groovy new file mode 100644 index 000000000..922d93c80 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/RequestScopeTest.groovy @@ -0,0 +1,22 @@ +package ru.vyarus.guicey.admin + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.admin.support.AdminRestApplication +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 03.09.2015 + */ +@TestDropwizardApp(value = AdminRestApplication, + config = 'src/test/resources/ru/vyarus/guicey/admin/simpleConfig.yml') +class RequestScopeTest extends Specification { + + def "Check request scope exist when access from admin context"() { + + when: "rest with request scope" + def res = new URL("http://localhost:8080/admin/rest/request/").getText() + then: "ok" + res == "hello" + } +} \ No newline at end of file diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/SimpleServerTest.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/SimpleServerTest.groovy new file mode 100644 index 000000000..1140da322 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/SimpleServerTest.groovy @@ -0,0 +1,49 @@ +package ru.vyarus.guicey.admin + +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.admin.support.AdminRestApplication + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +@TestDropwizardApp(value = AdminRestApplication, + config = 'src/test/resources/ru/vyarus/guicey/admin/simpleConfig.yml') +class SimpleServerTest extends AbstractTest { + + def "Check res access from user context"() { + + when: "opened rest" + def res = new URL("http://localhost:8080/application/rest/hybrid/hello").getText() + then: "ok" + res == "hello" + + when: "admin only rest" + new URL("http://localhost:8080/application/rest/hybrid/admin").getText() + then: "not accessible" + thrown(FileNotFoundException) + + when: "admin only rest (by class)" + new URL("http://localhost:8080/application/rest/admin/").getText() + then: "not accessible" + thrown(FileNotFoundException) + } + + def "Check access from admin context"() { + + when: "public rest" + def res = new URL("http://localhost:8080/admin/rest/hybrid/hello").getText() + then: "ok" + res == "hello" + + when: "admin rest" + res = new URL("http://localhost:8080/admin/rest/hybrid/admin").getText() + then: "ok" + res == "admin" + + when: "admin rest (by class annotation)" + res = new URL("http://localhost:8080/admin/rest/admin/").getText() + then: "ok" + res == "hello" + } +} \ No newline at end of file diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminResource.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminResource.groovy new file mode 100644 index 000000000..5e787d7cf --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminResource.groovy @@ -0,0 +1,20 @@ +package ru.vyarus.guicey.admin.support + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +@Path("/admin") +@ru.vyarus.guicey.admin.rest.AdminResource +class AdminResource { + + @GET + @Path("/") + public String hello() { + return "hello" + } + +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminRestApplication.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminRestApplication.groovy new file mode 100644 index 000000000..83f10ce64 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/AdminRestApplication.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.admin.support + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.admin.AdminRestBundle + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +class AdminRestApplication extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(getClass().package.name) + .bundles(new AdminRestBundle()) + .build() + ); + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/HybridResource.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/HybridResource.groovy new file mode 100644 index 000000000..e3a0bd101 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/HybridResource.groovy @@ -0,0 +1,25 @@ +package ru.vyarus.guicey.admin.support + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +@Path("/hybrid") +class HybridResource { + + @GET + @Path("/hello") + public String hello() { + return "hello" + } + + @GET + @Path("/admin") + @ru.vyarus.guicey.admin.rest.AdminResource + public String admin() { + return "admin" + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/ManualAdminRestPathApp.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/ManualAdminRestPathApp.groovy new file mode 100644 index 000000000..1804ba790 --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/ManualAdminRestPathApp.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.admin.support + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.admin.AdminRestBundle + +/** + * @author Vyacheslav Rusakov + * @since 08.08.2015 + */ +class ManualAdminRestPathApp extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(getClass().package.name) + .bundles(new AdminRestBundle("/rest/*")) + .build() + ); + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } +} diff --git a/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/RequestScopeResource.groovy b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/RequestScopeResource.groovy new file mode 100644 index 000000000..f8e4450de --- /dev/null +++ b/guicey-admin-rest/src/test/groovy/ru/vyarus/guicey/admin/support/RequestScopeResource.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.admin.support + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.servlet.http.HttpServletRequest +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 03.09.2015 + */ +@Path("/request") +class RequestScopeResource { + + @Inject + Provider requestProvider; + + @GET + @Path("/") + public String hello() { + // check request scoped beans work from admin context + requestProvider.get(); + return "hello" + } +} diff --git a/guicey-admin-rest/src/test/resources/ru/vyarus/guicey/admin/simpleConfig.yml b/guicey-admin-rest/src/test/resources/ru/vyarus/guicey/admin/simpleConfig.yml new file mode 100644 index 000000000..f3102fcd9 --- /dev/null +++ b/guicey-admin-rest/src/test/resources/ru/vyarus/guicey/admin/simpleConfig.yml @@ -0,0 +1,3 @@ +server: + type: simple + rootPath: '/rest/*' \ No newline at end of file diff --git a/guicey-eventbus/README.md b/guicey-eventbus/README.md new file mode 100644 index 000000000..036d25269 --- /dev/null +++ b/guicey-eventbus/README.md @@ -0,0 +1,176 @@ +# Guava EventBus integration + +### About + +Integrates [Guava EventBus](https://github.com/google/guava/wiki/EventBusExplained) with guice. + +Features: + +* EventBus available for injection (to publish events) +* Automatic registration of listener methods (annotated with `@Subscribe`) +* Console reporting of registered listeners + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-eventbus + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-eventbus:{guicey.version}' +``` + +Omit version if guicey BOM used. + +### Usage + +Register bundle: + +```java +GuiceBundle.builder() + .bundles(new EventBusBundle()) + ... +``` + +Create event: + +```java +public class MyEvent { + // some state +} +``` + +Inject `EventBus` to publish new events. + +```java +public class SomeService { + @Inject + private EVentBus eventbus; + + public void inSomeMethod() { + evetbus.post(new MyEvent()); + } +} +``` + +Listen for event: + +```java +public class SomeOtherService { + + @Subscribe + public void onEvent(MyEvent event) { + // handle event + } +} +``` + +After server start you should see all registered event listeners in log: + +``` +INFO [2016-12-01 12:31:02,819] ru.vyarus.guicey.eventbus.report.EventsReporter: EventBus subscribers = + + MyEvent + com.foo.something.SomeOtherService + +``` + +NOTE: only subscriptions of beans registered at the time of injector startup will be shown. + For example, if MyBean has subscription method but binding for it not declared (and noone depends on it) + then JIT binding will be created only somewhere later in time (when bean will be actually used) and + so listener registration happen after server startup and will not be shown in console report. + +##### Consuming multiple events + +Note that you can build event hierarchies and subscribe to some base event to receive any derived event. + +To receive all events use: + +```java +@Subscribe +public void onEvent(Object event){ +} +``` + +### Event bus + +By default, events will be handled synchronously (`bus.push()` waits while all subscribers processed). + +If you want events to be async use custom eventbus: + +```java +new EventBusBundle( + new AsyncEventBus(someExecutor) +) +``` + +By default, event listeners considered not thread safe and so no parallel events processing (for single method) +will be performed. To mark subscriber as thread safe use `@AllowConcurrentEvents`: + +```java +@Subscribe +@AllowConcurrentEvents +public void onEvent(MyEvent event) +``` + +If listener method will fail to process event (throw exception) then other listeners will still be processed +and failed listener exception will be logged. If you want to change this behaviour set custom exception +handler by creating custom eventbus instance: + +```java +new EventBusBundle( + new EventBus(customExceptionHandler) +) +``` + +### Listeners recognition + +Guice type listener used to intercept all beans instances. Each bean instance is registered in eventbus: +it's valid behaviour for eventbus and only beans with actual listener methods will be registered. + +But, it means that each bean class is checked: every method in class hierarchy. This is very fast and + does not make problems for most of the cases. But, if you want, you can reduce the scope for checking by + specifying custom class matcher: + + ```java +new EventBusBundle() + .withMatcher(Matchers.inSubpackage("some.package")) +``` + +This will only check beans in class and subpackages. + +If you want maximum performance, then you can add extra marker annotation (e.g. `@HasEvents`) and reduce +scope to just annotated classes: + + ```java +new EventBusBundle() + .withMatcher(Matchers.annotatedWith(HasEvents.class)) +``` + + +### Console reporting + +You can switch off console reporting (for example, if you have too much listeners): + +```java +new EventBusBundle().noReport() +``` + +Important moment: reporting has to use reflection to get subscribers list. If reflection will fail with newer guava version +(not yet supported), then simply disable reporting and everything will work. + +### Subscribers info bean + +Special guice bean registered and available for injection: `EventSubscribersInfo`. +With it you can get active listeners and used event types. Reporting use it for console report. +It may be useful for unit tests. + +As described above, internally it use reflection to access eventbus listeners map. diff --git a/guicey-eventbus/build.gradle b/guicey-eventbus/build.gradle new file mode 100644 index 000000000..5c23a39ed --- /dev/null +++ b/guicey-eventbus/build.gradle @@ -0,0 +1 @@ +description = "Guicey integration for Guava's EventBus" \ No newline at end of file diff --git a/guicey-eventbus/src/main/java/com/google/common/eventbus/SubscriptionIntrospector.java b/guicey-eventbus/src/main/java/com/google/common/eventbus/SubscriptionIntrospector.java new file mode 100644 index 000000000..380cefe8b --- /dev/null +++ b/guicey-eventbus/src/main/java/com/google/common/eventbus/SubscriptionIntrospector.java @@ -0,0 +1,101 @@ +package com.google.common.eventbus; + +import com.google.common.base.Preconditions; +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Provides access for registered event subscribers. + * + * @author Vyacheslav Rusakov + * @see EventSubscribersInfo for usage + * @since 02.12.2016 + */ +public class SubscriptionIntrospector { + + private static final String SUBSCRIBERS_FIELD = "subscribers"; + + private final EventBus eventbus; + private Map> subscribers; + + /** + * Create an introspector. + * + * @param eventbus event bus instance + */ + public SubscriptionIntrospector(final EventBus eventbus) { + this.eventbus = eventbus; + } + + /** + * @return event classes + */ + public Set getListenedEvents() { + return extractSubscribers().keySet(); + } + + /** + * @param event event class + * @return event subscribers + */ + public Set getSubscribers(final Class event) { + final Set res = new HashSet<>(); + final Set subscribers = extractSubscribers().get(event); + if (subscribers != null) { + for (Subscriber subs : subscribers) { + res.add(subs.target); + } + } + return res; + } + + /** + * @param event event class + * @return subscriber classes + * @see #getSubscribers(Class) for instances + */ + public Set getSubscriberTypes(final Class event) { + final Set res = new HashSet<>(); + for (Object obj : getSubscribers(event)) { + res.add(extractType(obj)); + } + return res; + } + + @SuppressWarnings("unchecked") + private Map> extractSubscribers() { + synchronized (this) { + if (subscribers == null) { + try { + final Field registryField = EventBus.class.getDeclaredField(SUBSCRIBERS_FIELD); + registryField.setAccessible(true); + final SubscriberRegistry registry = (SubscriberRegistry) registryField.get(eventbus); + + final Field subscribersField = SubscriberRegistry.class.getDeclaredField(SUBSCRIBERS_FIELD); + subscribersField.setAccessible(true); + + subscribers = (Map>) Preconditions + .checkNotNull(subscribersField.get(registry)); + } catch (Exception e) { + throw new IllegalStateException("Failed to access subscribers collection", e); + } + } + return subscribers; + } + } + + private Class extractType(final Object instance) { + Class cls = instance.getClass(); + while (cls.getSuperclass() != Object.class) { + if (!cls.getSimpleName().contains("$")) { + break; + } + cls = cls.getSuperclass(); + } + return cls; + } +} diff --git a/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/EventBusBundle.java b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/EventBusBundle.java new file mode 100644 index 000000000..0caed61bb --- /dev/null +++ b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/EventBusBundle.java @@ -0,0 +1,106 @@ +package ru.vyarus.guicey.eventbus; + +import com.google.common.eventbus.EventBus; +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.guicey.eventbus.module.EventBusModule; +import ru.vyarus.guicey.eventbus.module.TypeLiteralAdapterMatcher; +import ru.vyarus.guicey.eventbus.report.EventSubscribersReporter; +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo; + +/** + * Binds support for single (!) event bus. {@link EventBus} available for injection (to publish events). + * All guice beans with methods annotated with {@link com.google.common.eventbus.Subscribe} are + * automatically registered. All listeners subscribed before startup are reported to logs (may be disabled). + *

        + * If you want to customize default event bus, configure instance manually and provide instance in constructor: + *

        
        + *     new EventBusBundle(myCustomBus)
        + * 
        + *

        + * You can reduce amount of classes checked for listener methods by providing custom types matcher. For example, + *

        
        + *     new EventBusBundle()
        + *          .withMatcher(Matchers.inSubpackage("some.package"))
        + * 
        + *

        + * Reflection is used for registered listeners printing (no way otherwise to get registered subscribers). + * If there will be any problems with it, simply disable reporting. + *

        + * Only one bundle instance will be actually used (in case of multiple registrations). + * + * @author Vyacheslav Rusakov + * @see eventbus documentation + * @see EventSubscribersInfo for subscribers info access + * @since 12.10.2016 + */ +public class EventBusBundle extends UniqueGuiceyBundle { + + private final EventBus eventbus; + private Matcher> typeMatcher = Matchers.any(); + private boolean report = true; + + /** + * Register default event bus. Events processing is synchronous. + */ + public EventBusBundle() { + this(new EventBus("bus")); + } + + /** + * Registers custom event bus. Use this constructor to customize event bus or to switch to + * {@link com.google.common.eventbus.AsyncEventBus}. + * + * @param eventbus event bus instance + */ + public EventBusBundle(final EventBus eventbus) { + this.eventbus = eventbus; + } + + /** + * By default, all registered bean types are checked for listener methods. + * Listener check involves all methods in class and subclasses lookup. + * If you have too much beans which are not using eventbus, then it makes sense to reduce checked beans scope + * For example, check only beans in some package: {@code Matchers.inSubpackage("some.pacjage")}. + *

        + * The most restrictive (and faster) approach would be to introduce your annotation (e.g. {@code @EventListener}) + * and search for listeners only inside annotated classes ({@code Matchers.annotatedWith(EventListener.class)}. + * + * @param classMatcher class matcher to reduce classes checked for listener methods + * @return bundle instance for chained calls + */ + public EventBusBundle withMatcher(final Matcher> classMatcher) { + this.typeMatcher = new TypeLiteralAdapterMatcher(classMatcher); + return this; + } + + /** + * If you have a lot of listeners or events or simply don't want console reporting use this method. + *

        + * Disabling reporting will also disable reflective access to eventbus internals, so disable it if you have + * problems (for example, new guava version renamed field). + * + * @return bundle instance for chained calls + */ + public EventBusBundle noReport() { + report = false; + return this; + } + + @Override + public void run(final GuiceyEnvironment environment) { + environment.modules(new EventBusModule(eventbus, typeMatcher)); + + if (report) { + // report after application startup to count events, resolved from JIT-created services (not declared) + environment.onApplicationStartup(injector -> { + new EventSubscribersReporter( + injector.getInstance(EventSubscribersInfo.class)) + .report(); + }); + } + } +} diff --git a/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/EventBusModule.java b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/EventBusModule.java new file mode 100644 index 000000000..752521d87 --- /dev/null +++ b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/EventBusModule.java @@ -0,0 +1,60 @@ +package ru.vyarus.guicey.eventbus.module; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo; + +import jakarta.inject.Singleton; + +/** + * Module binds provided {@link EventBus} instance. Publishers should inject event bus for posting events. + * Listeners must only define method with event as argument and annotated with {@link Subscribe}. All guice beans + * with annotated methods registered automatically. + * + * @author Vyacheslav Rusakov + * @see EventSubscribersInfo guice bean registered for programmatic subscribers info access + * @since 12.10.2016 + */ +public class EventBusModule extends AbstractModule { + + private final EventBus eventbus; + private final Matcher> typeMatcher; + + /** + * Create event bus module. + * + * @param eventbus event bus instance + * @param typeMatcher matcher for classes to search listener methods in + */ + public EventBusModule(final EventBus eventbus, + final Matcher> typeMatcher) { + this.eventbus = eventbus; + this.typeMatcher = typeMatcher; + } + + @Override + protected void configure() { + bind(EventBus.class).toInstance(eventbus); + bind(EventSubscribersInfo.class).in(Singleton.class); + + bindListener(); + } + + @SuppressWarnings("unchecked") + private void bindListener() { + bindListener(typeMatcher, new TypeListener() { + @Override + public void hear(final TypeLiteral type, final TypeEncounter encounter) { + // register all beans: event bus will introspect each class and register found listeners + // duplicate registrations are valid (internal event bus cache will handle it) + encounter.register((InjectionListener) eventbus::register); + } + }); + } +} diff --git a/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/TypeLiteralAdapterMatcher.java b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/TypeLiteralAdapterMatcher.java new file mode 100644 index 000000000..7988fab2b --- /dev/null +++ b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/module/TypeLiteralAdapterMatcher.java @@ -0,0 +1,38 @@ +package ru.vyarus.guicey.eventbus.module; + +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; + +/** + * Wrapper for class matcher to be used for matching type literals. + * + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +public class TypeLiteralAdapterMatcher implements Matcher> { + private final Matcher> classMatcher; + + /** + * Create a type literal matcher. + * + * @param classMatcher class matcher + */ + public TypeLiteralAdapterMatcher(final Matcher> classMatcher) { + this.classMatcher = classMatcher; + } + + @Override + public boolean matches(final TypeLiteral typeLiteral) { + return classMatcher.matches(typeLiteral.getRawType()); + } + + @Override + public Matcher> and(final Matcher> other) { + throw new UnsupportedOperationException(); + } + + @Override + public Matcher> or(final Matcher> other) { + throw new UnsupportedOperationException(); + } +} diff --git a/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/report/EventSubscribersReporter.java b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/report/EventSubscribersReporter.java new file mode 100644 index 000000000..02f5c32cf --- /dev/null +++ b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/report/EventSubscribersReporter.java @@ -0,0 +1,68 @@ +package ru.vyarus.guicey.eventbus.report; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.NEWLINE; +import static ru.vyarus.dropwizard.guice.module.installer.util.Reporter.TAB; + +/** + * Reports subscribed event listeners. + * Note: it will report only known subscribers (because some beans may be instantiated lazily with guice JIT + * after server startup). + * + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +public class EventSubscribersReporter { + private final Logger logger = LoggerFactory.getLogger(EventSubscribersReporter.class); + + private final EventSubscribersInfo info; + + /** + * Create event bus subscribers reporter. + * + * @param info subscribers info + */ + public EventSubscribersReporter(final EventSubscribersInfo info) { + this.info = info; + } + + /** + * @return rendered events and subscribers report + */ + public String renderReport() { + final Set events = info.getListenedEvents(); + if (events.isEmpty()) { + return null; + } + + final List sortedEvents = new ArrayList<>(events); + sortedEvents.sort(Comparator.comparing(Class::getSimpleName)); + final StringBuilder res = new StringBuilder("EventBus subscribers = ") + .append(NEWLINE); + for (Class event : sortedEvents) { + res.append(NEWLINE).append(TAB).append(event.getSimpleName()).append(NEWLINE); + for (Class subs : info.getListenerTypes(event)) { + res.append(TAB).append(TAB).append(subs.getName()).append(NEWLINE); + } + } + return res.toString(); + } + + /** + * Print registered listeners to console. Do nothing if no known listeners. + */ + public void report() { + final String report = renderReport(); + if (report != null) { + logger.info(report); + } + } +} diff --git a/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/service/EventSubscribersInfo.java b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/service/EventSubscribersInfo.java new file mode 100644 index 000000000..0abd86a70 --- /dev/null +++ b/guicey-eventbus/src/main/java/ru/vyarus/guicey/eventbus/service/EventSubscribersInfo.java @@ -0,0 +1,64 @@ +package ru.vyarus.guicey.eventbus.service; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.SubscriptionIntrospector; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.Set; + +/** + * Holds registered event listeners. Will contain nothing if tracking disabled. + * Service registered in guice and may be injected directly (e.g. for unit tests). + * + * @author Vyacheslav Rusakov + * @since 12.10.2016 + */ +@Singleton +public class EventSubscribersInfo { + private final SubscriptionIntrospector introspector; + + /** + * Create event bus subscribers info. + * + * @param eventbus event bus instance + */ + @Inject + public EventSubscribersInfo(final EventBus eventbus) { + this.introspector = new SubscriptionIntrospector(eventbus); + } + + + /** + * May return not just event types, because method could listen for events abstract type or + * {@link Object} to receive all events. + * + * @return set of events with known subscribers or empty set + */ + public Set getListenedEvents() { + return introspector.getListenedEvents(); + } + + /** + * NOTE: method may return not all listeners, because some methods may listen for a range of events + * (by base class or {@link Object}). Only direct subscriptions are tracked. + * + * @param event event class to get listeners for + * @return collection of classes listening for event type, or empty list + */ + public Set getListenerTypes(final Class event) { + return introspector.getSubscriberTypes(event); + } + + /** + * NOTE: method may return not all listeners, because some methods may listen for a range of events + * (by base class or {@link Object}). Only direct subscriptions are tracked. + * + * @param event event class to get listeners for + * @return collection of instances listening for event type, or empty list + */ + public Set getListeners(final Class event) { + return introspector.getSubscribers(event); + } +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomBusTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomBusTest.groovy new file mode 100644 index 000000000..ff2ee38dc --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomBusTest.groovy @@ -0,0 +1,78 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.AsyncEventBus +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event2 +import ru.vyarus.guicey.eventbus.support.HasEvents +import spock.lang.Specification + +import jakarta.inject.Inject +import java.util.concurrent.Executors + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class CustomBusTest extends Specification { + + @Inject + EventBus bus + @Inject + Service service // trigger JIT binding + @Inject + EventSubscribersInfo info + + def "Check correct registration"() { + + expect: "listeners registered" + bus instanceof AsyncEventBus + info.getListenedEvents() == [Event1] as Set + info.getListenerTypes(Event1) == [Service] as Set + info.getListenerTypes(Event2).isEmpty() + + } + + def "Check publication"() { + + when: "publish event" + bus.post(new Event1()) + sleep(100) + then: "received" + service.event1 == 1 + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle(new AsyncEventBus(Executors.newSingleThreadExecutor()))) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @HasEvents + static class Service { + + int event1 + + @Subscribe + void onEvent1(Event1 event) { + event1++ + } + } +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomMatcherTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomMatcherTest.groovy new file mode 100644 index 000000000..b9e066813 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/CustomMatcherTest.groovy @@ -0,0 +1,91 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import com.google.inject.matcher.Matchers +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.AbstractEvent +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event2 +import ru.vyarus.guicey.eventbus.support.Event3 +import ru.vyarus.guicey.eventbus.support.HasEvents +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App.class) +class CustomMatcherTest extends Specification { + @Inject + EventBus bus + @Inject + Service service // trigger JIT binding + @Inject + ServiceNoEvents serviceNoEvents // trigger JIT binding + @Inject + EventSubscribersInfo info + + def "Check correct registration"() { + + expect: "listeners registered" + info.getListenedEvents() == [Event1] as Set + info.getListenerTypes(Event1) == [Service] as Set + info.getListenerTypes(Event2).isEmpty() + info.getListenerTypes(Event3).isEmpty() + info.getListenerTypes(AbstractEvent).isEmpty() + + } + + def "Check publication"() { + + when: "publish event" + bus.post(new Event1()) + then: "received" + service.event1 == 1 + serviceNoEvents.event1 == 0 + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle().withMatcher(Matchers.annotatedWith(HasEvents))) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @HasEvents + static class Service { + + int event1 + + @Subscribe + void onEvent1(Event1 event) { + event1++ + } + } + + static class ServiceNoEvents { + + int event1 + + @Subscribe + void onEvent1(Event1 event) { + event1++ + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/RenderEmptyTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/RenderEmptyTest.groovy new file mode 100644 index 000000000..0a87d418e --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/RenderEmptyTest.groovy @@ -0,0 +1,49 @@ +package ru.vyarus.guicey.eventbus + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.report.EventSubscribersReporter +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class RenderEmptyTest extends Specification { + @Inject + EventSubscribersInfo info + EventSubscribersReporter reporter + + void setup() { + reporter = new EventSubscribersReporter(info) + } + + def "Check print"() { + + expect: "not reported" + reporter.renderReport() == null + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle().noReport()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterMultiTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterMultiTest.groovy new file mode 100644 index 000000000..e07d30cb7 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterMultiTest.groovy @@ -0,0 +1,85 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.report.EventSubscribersReporter +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.AbstractEvent +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event3 +import ru.vyarus.guicey.eventbus.support.HasEvents +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class ReporterMultiTest extends Specification { + @Inject + EventBus bus + @Inject + Service service // trigger JIT binding + @Inject + EventSubscribersInfo info + EventSubscribersReporter reporter + + void setup() { + reporter = new EventSubscribersReporter(info) + } + + def "Check print"() { + + expect: "reported" + reporter.renderReport().replaceAll("\r", "") == """EventBus subscribers = + + AbstractEvent + ru.vyarus.guicey.eventbus.ReporterMultiTest\$Service + + Event1 + ru.vyarus.guicey.eventbus.ReporterMultiTest\$Service + + Event3 + ru.vyarus.guicey.eventbus.ReporterMultiTest\$Service +""" + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle().noReport()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @HasEvents + static class Service { + + @Subscribe + void onEvent1(Event1 event) { + } + + @Subscribe + void onEvent3(Event3 event) { + } + + @Subscribe + void onEvent21(AbstractEvent event) { + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterTest.groovy new file mode 100644 index 000000000..5bf1cef1a --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReporterTest.groovy @@ -0,0 +1,72 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.report.EventSubscribersReporter +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.HasEvents +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class ReporterTest extends Specification { + @Inject + EventBus bus + @Inject + Service service // trigger JIT binding + @Inject + EventSubscribersInfo info + EventSubscribersReporter reporter + + void setup() { + reporter = new EventSubscribersReporter(info) + } + + def "Check print"() { + + expect: "reported" + reporter.renderReport().replaceAll("\r", "") == """EventBus subscribers = + + Event1 + ru.vyarus.guicey.eventbus.ReporterTest\$Service +""" + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle().noReport()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @HasEvents + static class Service { + + int event1 + + @Subscribe + void onEvent1(Event1 event) { + event1++ + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReportingLogTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReportingLogTest.groovy new file mode 100644 index 000000000..88d82c00d --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/ReportingLogTest.groovy @@ -0,0 +1,66 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.Subscribe +import com.google.inject.AbstractModule +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.support.AbstractEvent +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event3 +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class ReportingLogTest extends Specification { + + def "Check correct registration"() { + + expect: "expecting log called" + true + + } + + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle()) + // need to force listeners registration to actually call reporting to console + .modules(new AbstractModule() { + @Override + protected void configure() { + bind(SubscribersInfoTest.Service).asEagerSingleton() + } + }) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + @Subscribe + void onEvent1(Event1 event) { + } + + @Subscribe + void onEvent3(Event3 event) { + } + + @Subscribe + void onEvent21(AbstractEvent event) { + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscribersInfoTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscribersInfoTest.groovy new file mode 100644 index 000000000..988a27964 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscribersInfoTest.groovy @@ -0,0 +1,102 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.Subscribe +import com.google.inject.Injector +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.jupiter.param.Jit +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.AbstractEvent +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event2 +import ru.vyarus.guicey.eventbus.support.Event3 +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App) +class SubscribersInfoTest extends Specification { + + // can't use @Inject here because it will not work on shared fields + // but shared state is important because there are two tests + @Shared + Service service + @Shared + Service2 service2 + @Inject + EventSubscribersInfo info + @Inject + Injector injector + + // trigger JIT binding + void setupSpec(@Jit Service service, @Jit Service2 service2) { + this.service = service + this.service2 = service2 + } + + def "Check correct tracking"() { + + expect: "listeners registered" + info.getListenedEvents() == [Event1, Event3, AbstractEvent] as Set + info.getListeners(Event1) == [service, service2] as Set + info.getListeners(Event2).isEmpty() + info.getListeners(Event3) == [service] as Set + info.getListeners(AbstractEvent) == [service] as Set + + } + + def "Check multiple instances"() { + + when: "create multiply instances (prototype scope)" + Service2 inst1 = injector.getInstance(Service2) + Service2 inst2 = injector.getInstance(Service2) + + then: "instances tracked" + info.getListeners(Event1) == [service, service2, inst1, inst2] as Set + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + @Subscribe + void onEvent1(Event1 event) { + } + + @Subscribe + void onEvent3(Event3 event) { + } + + @Subscribe + void onEvent21(AbstractEvent event) { + } + } + + static class Service2 { + + @Subscribe + void onEvent1(Event1 event) { + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscriptionTest.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscriptionTest.groovy new file mode 100644 index 000000000..cbe0ec6ab --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/SubscriptionTest.groovy @@ -0,0 +1,104 @@ +package ru.vyarus.guicey.eventbus + +import com.google.common.eventbus.EventBus +import com.google.common.eventbus.Subscribe +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.eventbus.service.EventSubscribersInfo +import ru.vyarus.guicey.eventbus.support.AbstractEvent +import ru.vyarus.guicey.eventbus.support.Event1 +import ru.vyarus.guicey.eventbus.support.Event2 +import ru.vyarus.guicey.eventbus.support.Event3 +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@TestGuiceyApp(App.class) +class SubscriptionTest extends Specification { + + @Inject + EventBus bus + @Inject + Service service // trigger JIT binding + @Inject + EventSubscribersInfo info + + def "Check correct registration"() { + + expect: "listeners registered" + info.getListenedEvents() == [Event1, Event3, AbstractEvent] as Set + info.getListenerTypes(Event1) == [Service] as Set + info.getListenerTypes(Event2).isEmpty() + info.getListenerTypes(Event3) == [Service] as Set + info.getListenerTypes(AbstractEvent) == [Service] as Set + + } + + def "Check publication"() { + + when: "publish first event" + bus.post(new Event1()) + then: "received" + service.event1 == 1 + service.event3 == 0 + service.event21 == 1 + + when: "publish second event" + bus.post(new Event2()) + then: "received" + service.event1 == 1 + service.event3 == 0 + service.event21 == 2 + + when: "publish third event" + bus.post(new Event3()) + then: "received" + service.event1 == 1 + service.event3 == 1 + service.event21 == 2 + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new EventBusBundle()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + int event1 + int event3 + int event21 + + @Subscribe + void onEvent1(Event1 event) { + event1++ + } + + @Subscribe + void onEvent3(Event3 event) { + event3++ + } + + @Subscribe + void onEvent21(AbstractEvent event) { + event21++ + } + } +} \ No newline at end of file diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/AbstractEvent.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/AbstractEvent.groovy new file mode 100644 index 000000000..07efb4697 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/AbstractEvent.groovy @@ -0,0 +1,8 @@ +package ru.vyarus.guicey.eventbus.support + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +abstract class AbstractEvent { +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event1.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event1.groovy new file mode 100644 index 000000000..57c1d42d0 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event1.groovy @@ -0,0 +1,8 @@ +package ru.vyarus.guicey.eventbus.support + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +class Event1 extends AbstractEvent { +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event2.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event2.groovy new file mode 100644 index 000000000..f4325b2aa --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event2.groovy @@ -0,0 +1,8 @@ +package ru.vyarus.guicey.eventbus.support + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +class Event2 extends AbstractEvent { +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event3.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event3.groovy new file mode 100644 index 000000000..44243b427 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/Event3.groovy @@ -0,0 +1,8 @@ +package ru.vyarus.guicey.eventbus.support + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +class Event3 { +} diff --git a/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/HasEvents.groovy b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/HasEvents.groovy new file mode 100644 index 000000000..5f2293a55 --- /dev/null +++ b/guicey-eventbus/src/test/groovy/ru/vyarus/guicey/eventbus/support/HasEvents.groovy @@ -0,0 +1,16 @@ +package ru.vyarus.guicey.eventbus.support + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2016 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@interface HasEvents { + +} \ No newline at end of file diff --git a/guicey-jdbi3/README.md b/guicey-jdbi3/README.md new file mode 100644 index 000000000..f181e91a0 --- /dev/null +++ b/guicey-jdbi3/README.md @@ -0,0 +1,357 @@ +# JDBI 3 integration + +> [Example app](https://github.com/xvik/dropwizard-guicey/tree/master/examples/ext-jdbi3) + +### About + +Integrates [JDBI3](http://jdbi.org/) with guice. Based on [dropwizard-jdbi3](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html) integration. + +Features: + +* JDBI instance available for injection +* Introduce unit of work concept, which is managed by annotations and guice aop (very like spring's @Transactional) +* Repositories (JDBI proxies for interfaces): + - installed automatically (when classpath scan enabled) + - are normal guice beans, supporting aop and participating in global (thread bound) transaction. + - no need to compose repositories anymore (e.g. with @CreateSqlObject) to gain single transaction. + - can reference guice beans (with annotated getters) +* Automatic installation for custom `RowMapper` + +Added installers: + +* [RepositoryInstaller](src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java) - sql proxies +* [MapperInstaller](src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java) - row mappers + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-jdbi3 + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-jdbi3:{guicey.version}' +``` + +Omit version if guicey BOM used. + +### Usage + +Register bundle: + +```java +GuiceBundle.builder() + .bundles(JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())) + ... +``` + +Here default JDBI instance will be created from database configuration (much like it's described in +[dropwizard documentation](https://www.dropwizard.io/en/release-4.0.x/manual/jdbi3.html)). + +Or build JDBI instance yourself: + +```java +JdbiBundle.forDbi((conf, env) -> locateDbi()) +``` + +Jdbi3 introduce plugins concept. Dropwizard will automatically register `SqlObjectPlugin`, `GuavaPlugin`, `JodaTimePlugin`. +If you need to install custom plugin: + +```java +JdbiBundle.forDbi((conf, env) -> locateDbi()) + .withPlugins(new H2DatabasePlugin()) +``` + +Also, If custom registration must be performed on jdbi instance: + +```java +JdbiBundle.forDbi((conf, env) -> locateDbi()) + .withConfig((jdbi) -> { jdbi.callSomething() }) +``` + +Such configuration block will be called just after jdbi instance creation (but before injector creation). + +#### Unit of work + +Unit of work concept states for: every database related operation must be performed inside unit of work. + +In JDBI such approach was implicit: you were always tied to initial handle. This lead to cumbersome usage of +sql object proxies: if you create it on-demand it would always create new handle; if you want to combine +multiple objects in one transaction, you have to always create them manually for each transaction. + +Integration removes these restrictions: dao (repository) objects are normal guice beans and transaction +scope is controlled by `@InTransaction` annotation (note that such name was intentional to avoid confusion with +JDBI own's Transaction annotation and more common Transactional annotations). + +At the beginning of unit of work, JDBI handle is created and bound to thread (thread local). +All repositories are simply using this bound handle and so share transaction inside unit of work. + +##### @InTransaction + +Annotation on method or class declares transactional scope. For example: + +```java +@Inject MyDAO dao + +@InTransaction +public Result doSomething() { + dao.select(); + ... +} +``` + +Transaction opened before doSomething() method and closed after it. +Dao call is also performed inside transaction. +If exception appears during execution, it's propagated and transaction rolled back. + +Nested annotations are allowed (they simply ignored). + +Note that unit of work is not the same as transaction scope (transaction scope could be less or equal to unit of work). +But, for simplicity, you may think of it as the same things, if you always use `@InTransaction` annotation. + +###### Transaction configuration + +Transaction isolation level and readonly flag could be defined with annotation: + +```java +@InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED) + +@InTransaction(readOnly = true) +``` + +In case of nested transactions error will be thrown if: + +* Current transaction level is different then nested one +* Current transaction is read only and nexted one is not (note that some drivers, like h2, ignore readOnly flag completely) + +For example: + +```java +@InTransaction +public void action() { + nestedAction(); +} + +@InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED) +public void nestedAction() { +... +} +``` + +When `action()` method called new transaction is created with default level +(usually READ_COMMITTED). When 'nestedAction()' is called exception will be thrown +because it's transaction level requirement (READ_UNCOMMITTED) contradict with current transaction. + +###### Custom transactional annotation + +If required, you may use your own annotation for transaction definition: + +```java +JdbiBundle.forDatabase((conf, env) -> conf.getDatabase()) + .withTxAnnotations(MyCustomTransactional.class); +``` + +Note that this will override default annotation support. If you want to support multiple annotations then specify +all of them: + +```java +JdbiBundle.forDatabase((conf, env) -> conf.getDatabase()) + .withTxAnnotations(InTransaction.class, MyCustomTransactional.class); +``` + +If you need to support transaction configuration with your annotation then: + +1. Add required properties into annotation itself (see `@InTransaction` as example). +2. Create implementation of `TxConfigFactory` (see `InTransactionTxConfigFactory` as example) +3. Register factory inside your annotation with `@TxConfigSupport(MyCustomAnnotationTxConfigFactory.class)` + +Your factory will be instantiated as guice bean so annotate it as Singleton, if possible +to avoid redundant instances creation. + +Configuration is resolved just once for each method, so yur factory will be called just once +for each annotated (with your custom annotation) method. + +##### Context Handle + +Inside unit of work you may reference current handle by using: + +```java +@Inject Provider +``` + +##### Manual transaction definition + +You may define transaction (with unit of work) without annotation using: + +```java +@Inject TransactionTempate template; +... +template.inTrasansaction((handle) -> doSomething()) +``` + +Note that inside such manual scope you may also call any repository bean, as it's absolutely the same definition as +with annotation. + +You can also specify transaction config (if required): + +```java +@Inject TransactionTempate template; +... +template.inTrasansaction( + new TxConfig().level(TransactionIsolationLevel.READ_UNCOMMITTED), + (handle) -> doSomething()) +``` + + +#### Repository + +Declare repository (interface or abstract class) as usual, using DBI annotations. +It only must be annotated with `@JdbiRepository` so installer +could recognize it and register in guice context. + +NOTE: singleton scope will be forced for repositories. + +```java +@JdbiRepository +@InTransaction +public interface MyRepository { + + @SqlQuery("select name from something where id = :id") + String findNameById(@Bind("id") int id); +} +``` + +Note the use of `@InTransaction`: it was used to be able to call repository methods without extra annotations +(the lowest transaction scope is repository itself). It will make beans "feel the same" as usual DBI on demand +sql object proxies. + +`@InTransaction` annotation is handled using guice aop. You can use any other guice aop related features. + +*Don't use DBI @Transaction and @CreateSqlObject annotations anymore*: probably they will even work, but they are not +needed now and may confuse. + +All installed repositories are reported into console: + +``` +INFO [2016-12-05 19:42:27,374] ru.vyarus.guicey.jdbi3.installer.repository.RepositoryInstaller: repositories = + + (ru.vyarus.guicey.jdbi3.support.repository.SampleRepository) +``` + +##### Manual bindings + +Repository can't be recognized from guice binding because repository type is abstract +and guice would complain about it. But repository can be recognized from the chain. + +For example, suppose there is a base interface `Storage` +and JDBI implementation is only one possible implementation: `JdbiStorage extends Storage`. +In this case you can bind: `bind(Storage.class).to(JdbiStorage.class)` and use +everywhere in code `@Inject Storage storage;` (installer would bind interface to implementation and +guice would be able to correctly track binding to the generated instance). + +Only in this case repository class could be recognized from guice binding (even if it's not declared as extension and +classpath scan not used). + +In all other cases, repository declaration would cause an error (to identify incorrect declaration). + +#### Laziness + +By default, JDBI proxies for declared repositories created only on first repository method call. +Lazy behaviour is important to take into account all registered JDBI extensions. Laziness also +slightly speeds up application startup. + +If required, you can enable eager initialization during bundle construction: + +```java +JdbiBundle.forDatabase((conf, env) -> conf.getDatabase()) + .withEagerInitialization() +``` + +In the eager mode all proxies would be constructed after application initialization (before web part initialization). + +#### Guice beans access + +You can access guice beans by annotating getter with `@Inject` (javax or guice): + +```java +@JdbiRepository +@InTransaction +public interface MyRepository { + + @Inject + MyOtherRepository getOtherRepo(); + + @SqlQuery("select name from something where id = :id") + String findNameById(@Bind("id") int id); + + default String doSomething(int id) { + String name = findNameById(id); + return getOtherRepo().doSOmethingWithName(name); + } +} +``` + +Here call to `getOtherRepo()` will return `MyOtherRepository` guice bean, which is actually +another proxy. + +#### Row mapper + +If you have custom implementations of `RowMapper`, it may be registered automatically. +You will be able to use injections there because mappers become ususal guice beans (singletons). +When classpath scan is enabled, such classes will be searched and installed automatically. + +```java +public class CustomMapper implements RowMapper { + @Override + Custom map(ResultSet rs, StatementContext ctx) throws SQLException { + // mapping here + return custom; + } +} +``` + +And now Custom type could be used for queries: + +```java +@JdbiRepository +@InTransaction +public interface CustomRepository { + + @SqlQuery("select * from custom where id = :id") + Custom findNameById(@Bind("id") int id); +} +``` + +All installed mappers are reported to console: + +``` +INFO [2016-12-05 20:02:25,399] ru.vyarus.guicey.jdbi3.installer.MapperInstaller: jdbi mappers = + + Sample (ru.vyarus.guicey.jdbi3.support.mapper.SampleMapper) +``` + +### Manual unit of work definition + +If, for some reason, you don't need transaction at some place, you can declare raw unit of work and use +assigned handle directly: + +```java +@Inject UnitManager manager; + +manager.beginUnit(); +try { + Handle handle = manager.get(); + // logic executed in unit of work but without transaction +} finally { + manager.endUnit(); +} +``` + +Repositories could also be called inside such manual unit (as unit of work is correctly started). \ No newline at end of file diff --git a/guicey-jdbi3/build.gradle b/guicey-jdbi3/build.gradle new file mode 100644 index 000000000..a87ba5322 --- /dev/null +++ b/guicey-jdbi3/build.gradle @@ -0,0 +1,12 @@ +description = "Guicey integration for JDBI 3" + +dependencies { + // with caffeine 3 (required for jdk 16 and above) + implementation 'io.dropwizard:dropwizard-jdbi3' + implementation ('ru.vyarus:guice-ext-annotations') { + exclude group: 'com.google.inject', module: 'guice' + } + + testImplementation 'org.flywaydb:flyway-core:9.22.3' + testImplementation 'com.h2database:h2:2.4.240' +} \ No newline at end of file diff --git a/guicey-jdbi3/src/main/java/org/jdbi/v3/core/TransactionalHandleSupplier.java b/guicey-jdbi3/src/main/java/org/jdbi/v3/core/TransactionalHandleSupplier.java new file mode 100644 index 000000000..8ad96c7be --- /dev/null +++ b/guicey-jdbi3/src/main/java/org/jdbi/v3/core/TransactionalHandleSupplier.java @@ -0,0 +1,66 @@ +package org.jdbi.v3.core; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import org.jdbi.v3.core.config.ConfigRegistry; +import org.jdbi.v3.core.extension.ExtensionContext; +import org.jdbi.v3.core.extension.HandleSupplier; + +import java.util.concurrent.Callable; + +/** + * Bridge have to lie in jdbi package in order have access to internal methods. Implementation is the same + * as in {@link ConstantHandleSupplier}, except handler and config are obtained dynamically. + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@Singleton +public class TransactionalHandleSupplier implements HandleSupplier { + + private final Jdbi jdbi; + private final Provider handleProvider; + + /** + * Create a transactional handle supplier. + * + * @param jdbi jdbi instance + * @param handleProvider handle provider + */ + @Inject + public TransactionalHandleSupplier(final Jdbi jdbi, final Provider handleProvider) { + this.jdbi = jdbi; + this.handleProvider = handleProvider; + } + + @Override + public Handle getHandle() { + return handleProvider.get(); + } + + @Override + public Jdbi getJdbi() { + return jdbi; + } + + @Override + public V invokeInContext(final ExtensionContext extensionContext, final Callable task) + throws Exception { + // implementation copied from ConstantHandleSupplier + final Handle handle = getHandle(); + final ExtensionContext oldExtensionContext = new ExtensionContext( + handle.getConfig(), handle.getExtensionMethod()); + try { + handle.acceptExtensionContext(extensionContext); + return task.call(); + } finally { + handle.acceptExtensionContext(oldExtensionContext); + } + } + + @Override + public ConfigRegistry getConfig() { + return jdbi.getConfig(); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/JdbiBundle.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/JdbiBundle.java new file mode 100644 index 000000000..d0d93aa17 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/JdbiBundle.java @@ -0,0 +1,186 @@ +package ru.vyarus.guicey.jdbi3; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import io.dropwizard.core.Configuration; +import io.dropwizard.db.PooledDataSourceFactory; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.spi.JdbiPlugin; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.guicey.jdbi3.dbi.ConfigAwareProvider; +import ru.vyarus.guicey.jdbi3.dbi.SimpleDbiProvider; +import ru.vyarus.guicey.jdbi3.installer.MapperInstaller; +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository; +import ru.vyarus.guicey.jdbi3.installer.repository.RepositoryInstaller; +import ru.vyarus.guicey.jdbi3.installer.repository.sql.SqlObjectProvider; +import ru.vyarus.guicey.jdbi3.module.JdbiModule; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; +import ru.vyarus.guicey.jdbi3.unit.UnitManager; + +import jakarta.inject.Provider; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Bundle activates JDBI3 support. To construct bundle use static builders with jdbi or database config providers. + * For example: {@code JdbiBundle.forDatabase((env, conf) -> conf.getDatabase())}. + *

        + * Bundle introduce unit of work concept for JDBI. All actions must perform inside unit of work. You may use + * {@link InTransaction} annotation to annotate classes or methods in order to wrap logic with unit of work + * (and actual transaction). Annotations could be nested: in this case upper most annotation declares transaction and + * nested are ignored (logic executed inside upper transaction). To declare unit of work without transaction + * use {@link UnitManager}. + *

        + * To manually declare transaction use {@link TransactionTemplate} bean. + *

        + * Custom installations: + *

          + *
        • Classes annotated with {@link JdbiRepository} are installed + * as guice beans, but provides usual functionality as JDBI sql proxies (just no need to always combine them).
        • + *
        • Classes implementing {@link org.jdbi.v3.core.mapper.RowMapper} are registered + * automatically.
        • + *
        + *

        + * Only one bundle instance will be actually used (in case of multiple registrations). + * + * @author Vyacheslav Rusakov + * @see UnitManager for manual unit of work definition + * @see TransactionTemplate for manual work with transactions + * @see ru.vyarus.dropwizard.guice.module.installer.feature.jersey.ResourceInstaller for sql object + * customization details + * @since 31.08.2018 + */ +@SuppressWarnings("PMD.ExcessiveImports") +public final class JdbiBundle extends UniqueGuiceyBundle { + + private final ConfigAwareProvider jdbi; + private List> txAnnotations = ImmutableList + .>builder() + .add(InTransaction.class) + .build(); + private List plugins = Collections.emptyList(); + private Consumer configurer; + private boolean eagerInit; + + private JdbiBundle(final ConfigAwareProvider jdbi) { + this.jdbi = jdbi; + } + + /** + * By default, {@link InTransaction} annotation registered. If you need to use different or more annotations + * provide all of them. Note, that you will need to provide {@link InTransaction} too if you want to use it too, + * otherwise it would not be supported. + * + * @param txAnnotations annotations to use as transaction annotations + * @return bundle instance for chained calls + */ + @SafeVarargs + public final JdbiBundle withTxAnnotations(final Class... txAnnotations) { + this.txAnnotations = Lists.newArrayList(txAnnotations); + return this; + } + + /** + * Note that dropwizard registers some plugins (sql objects, guava and jodatime). + * + * @param plugins extra jdbi plugins to register + * @return bundle instance for chained calls + */ + public JdbiBundle withPlugins(final JdbiPlugin... plugins) { + this.plugins = Arrays.asList(plugins); + return this; + } + + /** + * Manual jdbi instance configuration. Configuration will be called just after jdbi object creation + * (on run dropwizard phase), but before guice injector creation. + * + * @param configurer configuration action + * @return bundle instance for chained calls + */ + public JdbiBundle withConfig(final Consumer configurer) { + this.configurer = configurer; + return this; + } + + /** + * By default, repository beans (annotated with {@link JdbiRepository}) are initialized on first method call. + * Lazy initialization is required to properly add all registered jdbi extensions. Also, this slightly speed + * up startup. + *

        + * This option will enable eager repositories initialization after application startup. It may be important if + * execution time of first method call is important (e.g. due to some metrics). + * + * @return bundle instance for chained calls + */ + public JdbiBundle withEagerInitialization() { + this.eagerInit = true; + return this; + } + + @Override + public void initialize(final GuiceyBootstrap bootstrap) { + bootstrap.installers( + RepositoryInstaller.class, + MapperInstaller.class); + + } + + @Override + public void run(final GuiceyEnvironment environment) { + final Jdbi jdbi = this.jdbi.get(environment.configuration(), environment.environment()); + plugins.forEach(jdbi::installPlugin); + if (configurer != null) { + configurer.accept(jdbi); + } + + environment.modules(new JdbiModule(jdbi, txAnnotations)); + if (eagerInit) { + // eager repository proxies creation + environment.onApplicationStartup(this::performEagerInitialization); + } + } + + /** + * Builds bundle for custom JDBI instance. + * + * @param dbi JDBI instance provider + * @param configuration type + * @return bundle instance + */ + public static JdbiBundle forDbi(final ConfigAwareProvider dbi) { + return new JdbiBundle(dbi); + } + + /** + * Builds bundle, by using only database factory from configuration. + * + * @param db database configuration provider + * @param configuration type + * @return bundle instance + */ + public static JdbiBundle forDatabase( + final ConfigAwareProvider db) { + return forDbi(new SimpleDbiProvider<>(db)); + } + + @SuppressWarnings("PMD.UseDiamondOperator") + private void performEagerInitialization(final Injector injector) { + final Set proxies = injector.getInstance( + Key.get(new TypeLiteral>() { }, Names.named("jdbi3.proxies"))); + for (Provider proxy : proxies) { + proxy.get(); + } + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/ConfigAwareProvider.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/ConfigAwareProvider.java new file mode 100644 index 000000000..c53f66153 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/ConfigAwareProvider.java @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.jdbi3.dbi; + +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; + +/** + * Helper for implementing lazy initialization. Useful in initialization part where bundles are configured. + * For example, to construct some dropwizard integration object and use it in guice integrations later. + * + * @param provided object type + * @param configuration type + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@FunctionalInterface +public interface ConfigAwareProvider { + + /** + * Called to provide required object. + * + * @param configuration configuration instance + * @param environment environment instance + * @return object instance + */ + T get(C configuration, Environment environment); +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/SimpleDbiProvider.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/SimpleDbiProvider.java new file mode 100644 index 000000000..63a033964 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/dbi/SimpleDbiProvider.java @@ -0,0 +1,33 @@ +package ru.vyarus.guicey.jdbi3.dbi; + +import io.dropwizard.core.Configuration; +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.jdbi3.JdbiFactory; +import io.dropwizard.core.setup.Environment; +import org.jdbi.v3.core.Jdbi; + +/** + * Simple DBI configurer, requiring just database configuration. + * + * @param configuration type + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +public class SimpleDbiProvider implements ConfigAwareProvider { + + private final ConfigAwareProvider database; + + /** + * Create configuration-aware jdbi provider. + * + * @param database configuration provider + */ + public SimpleDbiProvider(final ConfigAwareProvider database) { + this.database = database; + } + + @Override + public Jdbi get(final C configuration, final Environment environment) { + return new JdbiFactory().build(environment, database.get(configuration, environment), "db"); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/inject/InjectionHandlerFactory.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/inject/InjectionHandlerFactory.java new file mode 100644 index 000000000..d9268a809 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/inject/InjectionHandlerFactory.java @@ -0,0 +1,60 @@ +package ru.vyarus.guicey.jdbi3.inject; + +import com.google.common.base.Preconditions; +import com.google.inject.Injector; +import jakarta.inject.Inject; +import org.jdbi.v3.core.extension.ExtensionHandler; +import org.jdbi.v3.core.extension.ExtensionHandlerFactory; +import org.jdbi.v3.core.extension.HandleSupplier; + +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Sql objects are forced to be interfaces now so it is impossible to inject guice bean (probably other proxy) + * with field injection. In order to workaround this limitation getter injection must be used: + * {@code @Inject MyBean getBean();}. Handler detects methods annotated with {@link Inject} or + * {@link com.google.inject.Inject} and return actual guice bean on method call. + * + * @author Vyacheslav Rusakov + * @since 17.09.2018 + */ +public class InjectionHandlerFactory implements ExtensionHandlerFactory { + + @Inject + private Injector injector; + + @Override + public boolean accepts(final Class extensionType, final Method method) { + return method.getAnnotation(Inject.class) != null + || method.getAnnotation(com.google.inject.Inject.class) != null; + } + + @Override + public Optional createExtensionHandler(final Class extensionType, final Method method) { + return Optional.of(new InjectionHandler(injector, method.getReturnType())); + } + + /** + * Handler provides guice managed instance on method call. + */ + private static class InjectionHandler implements ExtensionHandler { + private final Injector injector; + private final Class type; + + InjectionHandler(final Injector injector, final Class type) { + this.injector = Preconditions.checkNotNull(injector, "No injector"); + this.type = Preconditions.checkNotNull(type, "No type"); + Preconditions.checkState(type != Void.class && type != void.class, + "Only non void (getter) method could be anotated with @Inject in order" + + "to provide guice bean."); + } + + @Override + public Object invoke(final HandleSupplier handleSupplier, + final Object target, + final Object... args) throws Exception { + return injector.getInstance(type); + } + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java new file mode 100644 index 000000000..caeadfe61 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/MapperInstaller.java @@ -0,0 +1,74 @@ +package ru.vyarus.guicey.jdbi3.installer; + +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Stage; +import com.google.inject.multibindings.Multibinder; +import org.jdbi.v3.core.mapper.RowMapper; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.binding.BindingInstaller; +import ru.vyarus.dropwizard.guice.module.installer.util.FeatureUtils; +import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; +import ru.vyarus.guicey.jdbi3.module.MapperBinder; +import ru.vyarus.java.generics.resolver.GenericsResolver; + +import jakarta.inject.Singleton; +import java.util.Collections; +import java.util.List; + +/** + * Recognize classes implementing JDBI's {@link org.jdbi.v3.core.mapper.RowMapper} and register them. + * Register mappers as singletons. Reports all installed mappers to console. + *

        + * Mappers are normal guice beans and so may use constructor injection, aop etc. + * + * @author Vyacheslav Rusakov + * @see MapperBinder for actual installation + * @see row mappers doc + * @since 31.08.2018 + */ +public class MapperInstaller implements FeatureInstaller, BindingInstaller { + + private final Reporter reporter = new Reporter(MapperInstaller.class, "jdbi row mappers = "); + + @Override + public boolean matches(final Class type) { + return FeatureUtils.is(type, RowMapper.class); + } + + @Override + public void bind(final Binder binder, final Class type, final boolean lazy) { + binder.bind(type).in(Singleton.class); + register(binder, type); + } + + @Override + public void manualBinding(final Binder binder, final Class type, final Binding binding) { + register(binder, type); + } + + @SuppressWarnings("unchecked") + private void register(final Binder binder, final Class type) { + // just combine mappers in set and special bean, installed by module will bind it to dbi + Multibinder.newSetBinder(binder, RowMapper.class).addBinding() + .to((Class) type); + } + + @Override + public void extensionBound(final Stage stage, final Class type) { + if (stage != Stage.TOOL) { + final String target = GenericsResolver.resolve(type).type(RowMapper.class).genericAsString(0); + reporter.line("%-20s (%s)", target, type.getName()); + } + } + + @Override + public void report() { + reporter.report(); + } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("implements " + RowMapper.class.getSimpleName()); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/JdbiRepository.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/JdbiRepository.java new file mode 100644 index 000000000..f1d200f0d --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/JdbiRepository.java @@ -0,0 +1,24 @@ +package ru.vyarus.guicey.jdbi3.installer.repository; + +import java.lang.annotation.*; + +/** + * Annotation for marking JDBI dao classes (abstract classes or interfaces). + * Such classes will be recognized and installed by {@link RepositoryInstaller}. + * Annotated daos may be used as any other bean - no need to combine daos manually (like you have to do + * in normal JDBI), all daos automatically participate in current unit of work and so share the same transaction. + *

        + * Annotated classes may inject guice beans usign FIELD injection: constructor injection is impossible because + * jdbi creates proxy instance and it's not aware of guice. + *

        + * Annotated classes participate in guice aop! For example, transaction annotation may be used on abstract (!) dao + * classes to declare dao-wide unit of work. + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface JdbiRepository { +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java new file mode 100644 index 000000000..395507665 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/RepositoryInstaller.java @@ -0,0 +1,181 @@ +package ru.vyarus.guicey.jdbi3.installer.repository; + +import com.google.common.base.Preconditions; +import com.google.inject.Binder; +import com.google.inject.Binding; +import com.google.inject.Stage; +import com.google.inject.matcher.Matchers; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.name.Names; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import ru.vyarus.dropwizard.guice.debug.report.guice.util.GuiceModelUtils; +import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; +import ru.vyarus.dropwizard.guice.module.installer.install.binding.BindingInstaller; +import ru.vyarus.dropwizard.guice.module.installer.util.Reporter; +import ru.vyarus.guice.ext.core.generator.DynamicClassGenerator; +import ru.vyarus.guicey.jdbi3.installer.repository.sql.SqlObjectProvider; +import ru.vyarus.guicey.jdbi3.module.NoSyntheticMatcher; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; +import ru.vyarus.guicey.jdbi3.unit.UnitManager; +import ru.vyarus.java.generics.resolver.GenericsResolver; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Recognize classes annotated with {@link JdbiRepository} and register them. Such classes may be then + * injected as usual beans and used as usual daos. All daos will participate in the thread-bound transaction, + * declared by transaction annotation, transaction template or manually with unit manager. + *

        + * Dao may use any guice-related annotations because beans participate in guice aop. This is done by creating + * special guice-managed proxy class (where guice could apply aop). These proxies delegate all method calls to + * JDBI-managed proxies. + *

        + * Manual guice bindings are not allowed, except one case: {@code bind(Base.class).to(Repo.class)} where + * {@code Repo.class} is a recognizable (annotated) repository. This case useful for generifying repositories + * (so implementation with exact queries could be pluggable). + * + * @author Vyacheslav Rusakov + * @see InTransaction default annotation + * @see TransactionTemplate for template usage + * @see UnitManager for low level usage without transaction + * @since 31.08.2018 + */ +public class RepositoryInstaller implements FeatureInstaller, BindingInstaller { + + private final Reporter reporter = new Reporter(RepositoryInstaller.class, "repositories = "); + + private final Set bound = new HashSet<>(); + + @Override + public boolean matches(final Class type) { + final boolean res = type.getAnnotation(JdbiRepository.class) != null; + if (res) { + validateCorrectness(type); + } + return res; + } + + @Override + public void bind(final Binder binder, final Class type, final boolean lazy) { + Preconditions.checkState(!lazy, "@LazyBinding not supported"); + + generateRepository(binder, type); + } + + @Override + public void manualBinding(final Binder binder, final Class type, final Binding binding) { + if (binding.getKey().getTypeLiteral().getRawType() == type) { + // bind(type) case - guice will fail in any case but this way the message would be better + // bind(type).to(impl) - extension could be still found by classpath scan and registered failing guice + // better use bindings override (more obvious) + throw new UnsupportedOperationException(String.format( + "JDBI repository %s can't be installed from binding: %s", + type.getSimpleName(), GuiceModelUtils.getDeclarationSource(binding).toString())); + } else { + // bind(something).to(type) - binding of something to repo (in this case additional binding could be + // added) + generateRepository(binder, type); + } + } + + @Override + public void extensionBound(final Stage stage, final Class type) { + if (stage != Stage.TOOL) { + reporter.line(String.format("(%s)", type.getName())); + } + } + + @Override + public void report() { + reporter.report(); + } + + @Override + public List getRecognizableSigns() { + return Collections.singletonList("@" + JdbiRepository.class + " on class"); + } + + @SuppressWarnings({"unchecked", "checkstyle:Indentation", "PMD.UseDiamondOperator"}) + private void generateRepository(final Binder binder, final Class type) { + // avoid duplicate bindings from classpath scan and binding + if (bound.contains(type)) { + return; + } + + // jdbi on demand proxy creator: laziness required to wait for global configuration complete + // to let proxy factory create method configs with all global configurations (mappers) + final SqlObjectProvider jdbiProxy = new SqlObjectProvider(type); + binder.requestInjection(jdbiProxy); + + // collect proxies to be able to eagerly bootstrap them (and avoid slow first proxy execution) + Multibinder.newSetBinder(binder, SqlObjectProvider.class, Names.named("jdbi3.proxies")) + .addBinding().toInstance(jdbiProxy); + + // prepare non abstract implementation class (instantiated by guice) + final Class guiceType = DynamicClassGenerator.generate(type); + binder.bind(type).to(guiceType).in(Singleton.class); + + // interceptor registered for each dao and redirect calls to actual jdbi proxy + // (at this point all guice interceptors are already involved) + binder.bindInterceptor(Matchers.subclassesOf(type), NoSyntheticMatcher.instance(), + // exact class instead of compact lambda to make AOP report more informative + new JdbiProxyRedirect(jdbiProxy)); + + // without it, on reporting phase binding would be cached and not generated on real run + if (binder.currentStage() != Stage.TOOL) { + bound.add(type); + } + } + + @SuppressWarnings("unchecked") + private void validateCorrectness(final Class type) { + // repository base interfaces must not be annotated because in case of classpath scan they would + // also be registered as repositories and will ruin AOP appliance + for (Class check : GenericsResolver.resolve(type).getGenericsInfo().getComposingTypes()) { + if (!check.equals(type) && check.isAnnotationPresent(JdbiRepository.class)) { + throw new IllegalStateException(String.format( + "Incorrect repository %s declaration: base interface %s is also annotated with @%s which may " + + "break AOP mappings. Only root repository class must be annotated.", + type.getSimpleName(), + check.getSimpleName(), + JdbiRepository.class.getSimpleName())); + } + } + } + + /** + * Guice interceptor redirects calls from guice repository bean into jdbi proxy instance. + */ + public static class JdbiProxyRedirect implements MethodInterceptor { + + private final Provider jdbiProxy; + + /** + * Create jdbi proxy interceptor. + * + * @param jdbiProxy jdbi proxy provider + */ + public JdbiProxyRedirect(final Provider jdbiProxy) { + this.jdbiProxy = jdbiProxy; + } + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + try { + return invocation.getMethod().invoke(jdbiProxy.get(), invocation.getArguments()); + } catch (InvocationTargetException th) { + // avoid exception wrapping (simpler to handle outside) + throw th.getCause(); + } + } + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/sql/SqlObjectProvider.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/sql/SqlObjectProvider.java new file mode 100644 index 000000000..c3745168b --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/installer/repository/sql/SqlObjectProvider.java @@ -0,0 +1,78 @@ +package ru.vyarus.guicey.jdbi3.installer.repository.sql; + +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.TransactionalHandleSupplier; +import org.jdbi.v3.core.extension.Extensions; +import org.jdbi.v3.core.extension.NoSuchExtensionException; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +/** + * Factory re-implements {@code Jdbi.onDemand(Class)} in order to create proxy, using transactional handler + * (jdbi on-demand objects supposed to open-close connection on each call). + *

        + * Proxy is not created immediately because during proxy creation config is created for each method and so + * if some global row mapper will be registered after this moment, method config will not know about it. + * Provider is created just before injector creation and all mappers are registered just after injector creation, + * so without laziness nothing would work as planned. + * + * @param sql proxy type + * @author Vyacheslav Rusakov + * @since 13.09.2018 + */ +public class SqlObjectProvider implements Provider { + + @Inject + private Jdbi jdbi; + @Inject + private TransactionalHandleSupplier handleProvider; + + private final Class extensionType; + + private volatile T res; + + /** + * Dao proxy provider. + * + * @param extensionType dao class + */ + public SqlObjectProvider(final Class extensionType) { + this.extensionType = extensionType; + } + + @Override + public T get() { + // lazy sql proxy creation + if (res == null) { + synchronized (this) { + if (res == null) { + res = create(); + } + } + } + return res; + } + + /** + * Method used only for testing. + * + * @return true if jdbi proxy created, false if not + */ + public boolean isInitialized() { + return res != null; + } + + private T create() { + // Jdbi::onDemand(Class) + if (!extensionType.isInterface()) { + throw new IllegalArgumentException("On-demand extensions are only supported for interfaces."); + } + + // OnDemandExtensions::create(Jdbi, Class) + // We don't need to create jdk proxy here - extension instance is enough. + return jdbi.getConfig(Extensions.class) + .findFor(extensionType, handleProvider) + .orElseThrow(() -> new NoSuchExtensionException(extensionType)); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/JdbiModule.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/JdbiModule.java new file mode 100644 index 000000000..4d0beb8e4 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/JdbiModule.java @@ -0,0 +1,89 @@ +package ru.vyarus.guicey.jdbi3.module; + +import com.google.common.base.Preconditions; +import com.google.inject.AbstractModule; +import com.google.inject.Stage; +import com.google.inject.matcher.Matchers; +import com.google.inject.multibindings.Multibinder; +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.TransactionalHandleSupplier; +import org.jdbi.v3.core.extension.Extensions; +import org.jdbi.v3.core.mapper.RowMapper; +import ru.vyarus.guicey.jdbi3.inject.InjectionHandlerFactory; +import ru.vyarus.guicey.jdbi3.installer.repository.RepositoryInstaller; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; +import ru.vyarus.guicey.jdbi3.tx.aop.TransactionalInterceptor; +import ru.vyarus.guicey.jdbi3.unit.UnitManager; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Jdbi support guice module. Binds {@link Jdbi} for injection. + * Introduce unit of work concept for JDBI: thread-bound handle must be created in order to access db (work with db + * only inside unit of work). Assumed that repositories will be installed with + * {@link RepositoryInstaller}, which will customize instances to + * support unit of work. Also, customized instances support guice aop. + *

        + * It is assumed that in most cases unit of work will be defined together with transaction using transaction + * annotation (one or more). By default, only {@link InTransaction} annotation will be + * recognized. + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +public class JdbiModule extends AbstractModule { + private final Jdbi jdbi; + private final List> txAnnotations; + + /** + * Create jdbi module. + * + * @param jdbi jdbi instance + * @param txAnnotations transaction annotations + */ + public JdbiModule(final Jdbi jdbi, final List> txAnnotations) { + Preconditions.checkState(!txAnnotations.isEmpty(), + "Provide at least one transactional annotation"); + this.jdbi = jdbi; + this.txAnnotations = txAnnotations; + } + + @Override + protected void configure() { + // avoid handlers registration under tool stage execution - could lead to NPEs + if (binder().currentStage() != Stage.TOOL) { + // allow using guice beans inside proxies with getters, annotated by @Inject + final InjectionHandlerFactory gettersInjector = new InjectionHandlerFactory(); + requestInjection(gettersInjector); + jdbi.getConfig(Extensions.class).registerHandlerFactory(gettersInjector); + } + + bind(Jdbi.class).toInstance(jdbi); + + // init empty collection for case when no mappers registered + Multibinder.newSetBinder(binder(), RowMapper.class); + bind(MapperBinder.class).asEagerSingleton(); + + // unit of work support + bind(UnitManager.class); + bind(Handle.class).toProvider(UnitManager.class); + // transactions support + // supplier provides correct handler into jdbi sql proxies + bind(TransactionalHandleSupplier.class); + bind(TransactionTemplate.class); + + bindAnnotationsSupport(); + } + + private void bindAnnotationsSupport() { + final TransactionalInterceptor interceptor = new TransactionalInterceptor(txAnnotations); + requestInjection(interceptor); + txAnnotations.forEach(it -> { + bindInterceptor(Matchers.annotatedWith(it), NoSyntheticMatcher.instance(), interceptor); + bindInterceptor(Matchers.any(), Matchers.annotatedWith(it), interceptor); + }); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/MapperBinder.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/MapperBinder.java new file mode 100644 index 000000000..6aca71fdb --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/MapperBinder.java @@ -0,0 +1,30 @@ +package ru.vyarus.guicey.jdbi3.module; + +import com.google.inject.Inject; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.mapper.RowMapper; + +import java.util.Set; + +/** + * Supplements {@link ru.vyarus.guicey.jdbi3.installer.MapperInstaller}: installer recognize and report found + * mappers and this bean will actually register resolved mappers in dbi instance. + *

        + * Delayed initialization used to simplify access to DBI instance (in installer it was hard to do). + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +public class MapperBinder { + + /** + * Create mapper binder. + * + * @param dbi jdbi instance + * @param mappers row mappers + */ + @Inject + public MapperBinder(final Jdbi dbi, final Set mappers) { + mappers.forEach(dbi::registerRowMapper); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/NoSyntheticMatcher.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/NoSyntheticMatcher.java new file mode 100644 index 000000000..883175725 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/module/NoSyntheticMatcher.java @@ -0,0 +1,31 @@ +package ru.vyarus.guicey.jdbi3.module; + +import com.google.inject.matcher.Matcher; + +import java.lang.reflect.Method; + +/** + * Matcher to filter synthetic methods (to avoid warnings on aop proxies creation). + * + * @author Vyacheslav Rusakov + * @since 17.09.2018 + */ +public final class NoSyntheticMatcher implements Matcher { + + private static final NoSyntheticMatcher NO_SYNTHETIC_MATCHER = new NoSyntheticMatcher(); + + private NoSyntheticMatcher() { + } + + /** + * @return method matcher for filtering synthetic methods + */ + public static NoSyntheticMatcher instance() { + return NO_SYNTHETIC_MATCHER; + } + + @Override + public boolean matches(final Method method) { + return !method.isSynthetic(); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/InTransaction.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/InTransaction.java new file mode 100644 index 000000000..2e600af47 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/InTransaction.java @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.jdbi3.tx; + +import org.jdbi.v3.core.transaction.TransactionIsolationLevel; +import ru.vyarus.guicey.jdbi3.tx.aop.config.InTransactionTxConfigFactory; +import ru.vyarus.guicey.jdbi3.tx.aop.config.TxConfigSupport; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for unit of work and transaction declaration. Code executed under the scope of annotation will + * share the same transaction (and handle). + *

        + * Use on class to mark all methods as transactional. + *

        + * Support nesting: nested annotated elements will participate in outer transaction (and so exceptions will rollback + * entire transaction). If nested transaction configuration contradict with ongoing transaction then exception + * will be thrown (e.g. different isolation level or write required under read only transaction). + *

        + * NOTE: jdbi transaction annotation ({@link org.jdbi.v3.sqlobject.transaction.Transaction}) is not used to avoid + * internal jdbi transaction handling mechanism which may contradict with guice-central transactional mechanism + * (because simply jdbi is not aware of it and did not expect anyone to manage transaction instead). + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@TxConfigSupport(InTransactionTxConfigFactory.class) +public @interface InTransaction { + + /** + * @return the transaction isolation level. If not specified, invoke with the default isolation level. + */ + TransactionIsolationLevel value() default TransactionIsolationLevel.UNKNOWN; + /** + * Set the connection readOnly property before the transaction starts, and restore it before it returns. + * Databases may use this as a performance or concurrency hint. + * @return whether the transaction is read only + */ + boolean readOnly() default false; +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TransactionTemplate.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TransactionTemplate.java new file mode 100644 index 000000000..712d79b58 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TransactionTemplate.java @@ -0,0 +1,119 @@ +package ru.vyarus.guicey.jdbi3.tx; + + +import com.google.common.base.Throwables; +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.HandleCallback; +import org.jdbi.v3.core.transaction.TransactionException; +import org.jdbi.v3.core.transaction.TransactionIsolationLevel; +import ru.vyarus.guicey.jdbi3.unit.UnitManager; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +/** + * Transaction template used to both declare unit of work and start transaction. + * If called inside of transaction then provided action will be simply executed as transaction is already managed + * somewhere outside. In case of exception, it's propagated and transaction rolled back. + *

        + * Usage: + *

        
        + *    {@literal @}Inject TransactionTemplate template;
        + *     ...
        + *     template.inTransaction(() -> doSoemStaff())
        + * 
        + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@Singleton +public class TransactionTemplate { + + private final UnitManager manager; + + /** + * Create transactional template. + * + * @param manager unit manager + */ + @Inject + public TransactionTemplate(final UnitManager manager) { + this.manager = manager; + } + + /** + * Shortcut for {@link #inTransaction(TxConfig, TxAction)} for calling action with default transaction config. + * + * @param action action to execute + * @param return type + * @return action result + */ + public T inTransaction(final TxAction action) { + return inTransaction(new TxConfig(), action); + } + + /** + * Wraps provided action with unit of work and transaction. If called under already started transaction + * then action will be called directly. + *

        + * NOTE: If unit of work was started manually (using {@link UnitManager}, but without transaction started, + * then action will be simply executed without starting transaction. This was done for rare situations + * when logic must be performed without transaction and transaction annotation will simply indicate unit of work. + * + * @param config transaction config + * @param action action to execute + * @param return type + * @return action result + */ + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + public T inTransaction(final TxConfig config, final TxAction action) { + if (manager.isUnitStarted()) { + // already started + try { + return inCurrentTransaction(config, action); + } catch (Throwable th) { + Throwables.throwIfUnchecked(th); + throw new RuntimeException(th); + } + } else { + manager.beginUnit(); + try { + return inNewTransaction(config, action); + } finally { + manager.endUnit(); + } + } + } + + private T inCurrentTransaction(final TxConfig config, final TxAction action) throws Exception { + // mostly copies org.jdbi.v3.sqlobject.transaction.internal.TransactionDecorator logic + final Handle h = manager.get(); + if (config.isLevelSet()) { + final TransactionIsolationLevel currentLevel = h.getTransactionIsolationLevel(); + if (currentLevel != config.getLevel()) { + throw new TransactionException("Tried to execute nested @Transaction(" + config.getLevel() + "), " + + "but already running in a transaction with isolation level " + currentLevel + "."); + } + } + if (h.isReadOnly() && !config.isReadOnly()) { + throw new TransactionException("Tried to execute a nested @Transaction(readOnly=false) " + + "inside a readOnly transaction"); + } + return action.execute(h); + } + + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + private T inNewTransaction(final TxConfig config, final TxAction action) { + final Handle h = manager.get(); + h.setReadOnly(config.isReadOnly()); + final HandleCallback callback = handle -> { + try { + return action.execute(handle); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + }; + return config.isLevelSet() ? h.inTransaction(config.getLevel(), callback) : h.inTransaction(callback); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxAction.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxAction.java new file mode 100644 index 000000000..1c66b4843 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxAction.java @@ -0,0 +1,24 @@ +package ru.vyarus.guicey.jdbi3.tx; + +import org.jdbi.v3.core.Handle; + +/** + * Transaction action passed to transaction template. + * + * @param return type + * @author Vyacheslav Rusakov + * @see TransactionTemplate for usage + * @since 31.08.2018 + */ +@FunctionalInterface +public interface TxAction { + + /** + * Called under transaction. Exceptions are propagated and cause transaction rollback. + * + * @param handle current JDBI handle under unit of work + * @return action result + * @throws Exception on errors (used to move exceptions handling outside of action) + */ + T execute(Handle handle) throws Exception; +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxConfig.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxConfig.java new file mode 100644 index 000000000..23c008913 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/TxConfig.java @@ -0,0 +1,56 @@ +package ru.vyarus.guicey.jdbi3.tx; + +import org.jdbi.v3.core.transaction.TransactionIsolationLevel; + +/** + * Transaction configuration. If transaction is already started then configuration is just checked for compatibility + * with current transaction (e.g. same isolation required or non read only transaction under readonly one). + * + * @author Vyacheslav Rusakov + * @since 17.09.2018 + */ +@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") +public final class TxConfig { + + private TransactionIsolationLevel level = TransactionIsolationLevel.UNKNOWN; + private boolean readOnly; + + /** + * @return configured isolation level + */ + public TransactionIsolationLevel getLevel() { + return level; + } + + /** + * @return true for read only transaction + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * @return true when non default level set + */ + public boolean isLevelSet() { + return level != TransactionIsolationLevel.UNKNOWN; + } + + /** + * @param level transaction isolation level + * @return config itself for chained calls + */ + public TxConfig level(final TransactionIsolationLevel level) { + this.level = level; + return this; + } + + /** + * @param readOnly true for read only transaction + * @return config itself for chained calls + */ + public TxConfig readOnly(final boolean readOnly) { + this.readOnly = readOnly; + return this; + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/TransactionalInterceptor.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/TransactionalInterceptor.java new file mode 100644 index 000000000..a0d745094 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/TransactionalInterceptor.java @@ -0,0 +1,124 @@ +package ru.vyarus.guicey.jdbi3.tx.aop; + +import com.google.common.base.Throwables; +import com.google.inject.Injector; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; +import ru.vyarus.guicey.jdbi3.tx.TxConfig; +import ru.vyarus.guicey.jdbi3.tx.aop.config.TxConfigFactory; +import ru.vyarus.guicey.jdbi3.tx.aop.config.TxConfigSupport; + +import jakarta.inject.Inject; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Intercept transaction annotations usage and applies {@link TransactionTemplate} around method call. + * Transaction config could be obtained from annotation, if it supports it. + * + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +public class TransactionalInterceptor implements MethodInterceptor { + + private static final ReentrantLock LOCK = new ReentrantLock(); + + private final Map, Class> txConfigFactories + = new HashMap<>(); + // cache used to avoid annotations introspection on each call + private final Map methodCache = new HashMap<>(); + + @Inject + private TransactionTemplate template; + @Inject + private Injector injector; + + /** + * Create a transactional interceptor. + * + * @param txAnnotations transactional annotations + */ + public TransactionalInterceptor(final List> txAnnotations) { + findConfigurableAnnotations(txAnnotations); + } + + @Override + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + public Object invoke(final MethodInvocation invocation) throws Throwable { + final TxConfig config = checkTxConfig(invocation.getMethod()); + return template.inTransaction(config, handle -> { + try { + return invocation.proceed(); + } catch (Throwable throwable) { + Throwables.throwIfUnchecked(throwable); + throw new RuntimeException(throwable); + } + }); + } + + private void findConfigurableAnnotations(final List> txAnnotations) { + for (Class ann : txAnnotations) { + if (ann.isAnnotationPresent(TxConfigSupport.class)) { + txConfigFactories.put(ann, ann.getAnnotation(TxConfigSupport.class).value()); + } + } + } + + private TxConfig checkTxConfig(final Method method) { + final String methodIdentity = (method.getDeclaringClass().getName() + " " + method.toString()).intern(); + TxConfig cfg = methodCache.get(methodIdentity); + if (cfg == null) { + LOCK.lock(); + try { + if (methodCache.get(methodIdentity) != null) { + // cfg could be stored while waiting for lock + cfg = methodCache.get(methodIdentity); + } else { + cfg = buildConfig(method); + methodCache.put(methodIdentity, cfg); + } + } finally { + LOCK.unlock(); + } + } + return cfg; + } + + @SuppressWarnings("unchecked") + private TxConfig buildConfig(final Method method) { + TxConfig res = null; + if (!txConfigFactories.isEmpty()) { + // search on method first + Annotation txAnn = findAnnotation(method, txConfigFactories.keySet()); + + if (txAnn == null) { + // look on type + txAnn = findAnnotation(method.getDeclaringClass(), txConfigFactories.keySet()); + } + + if (txAnn != null) { + final TxConfigFactory factory = injector.getInstance(txConfigFactories.get(txAnn.annotationType())); + res = factory.build(txAnn); + } + } + // using default config to avoid re-introspection + return res == null ? new TxConfig() : res; + } + + private Annotation findAnnotation(final AnnotatedElement obj, + final Collection> anns) { + for (Class ann : anns) { + if (obj.isAnnotationPresent(ann)) { + return obj.getAnnotation(ann); + } + } + return null; + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/InTransactionTxConfigFactory.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/InTransactionTxConfigFactory.java new file mode 100644 index 000000000..688e16fdd --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/InTransactionTxConfigFactory.java @@ -0,0 +1,23 @@ +package ru.vyarus.guicey.jdbi3.tx.aop.config; + +import ru.vyarus.guicey.jdbi3.tx.InTransaction; +import ru.vyarus.guicey.jdbi3.tx.TxConfig; + +import jakarta.inject.Singleton; + +/** + * Transactional config support for default {@link InTransaction} annotation. + * + * @author Vyacheslav Rusakov + * @since 28.09.2018 + */ +@Singleton +public class InTransactionTxConfigFactory implements TxConfigFactory { + + @Override + public TxConfig build(final InTransaction annotation) { + return new TxConfig() + .level(annotation.value()) + .readOnly(annotation.readOnly()); + } +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigFactory.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigFactory.java new file mode 100644 index 000000000..75ccaa3bf --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigFactory.java @@ -0,0 +1,29 @@ +package ru.vyarus.guicey.jdbi3.tx.aop.config; + +import ru.vyarus.guicey.jdbi3.tx.TxConfig; + +import java.lang.annotation.Annotation; + +/** + * Factory converts transaction parameters from annotation into common tx config object. + * Declared with {@link TxConfigSupport} directly on target annotation class. + *

        + * IMPORTANT: Factory is obtained from guice context so prefer annotating it with {@link jakarta.inject.Singleton} + * to avoid redundant instantiations (if factory is stateless). + *

        + * Resolved config is cached for target method to avoid duplicate resolutions. + * + * @param annotation type + * @author Vyacheslav Rusakov + * @see ru.vyarus.guicey.jdbi3.tx.aop.TransactionalInterceptor + * @since 18.09.2018 + */ +@FunctionalInterface +public interface TxConfigFactory { + + /** + * @param annotation annotation to read configuration from + * @return tx config object (may be null) + */ + TxConfig build(T annotation); +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigSupport.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigSupport.java new file mode 100644 index 000000000..929e698a2 --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/tx/aop/config/TxConfigSupport.java @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.jdbi3.tx.aop.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used with transactional annotations with transaction config (like + * {@link ru.vyarus.guicey.jdbi3.tx.InTransaction}) in order to apply specified configuration. + *

        + * Note that transactional annotation may not provide any configurations and in this case default + * transaction configuration will be always used. + * + * @author Vyacheslav Rusakov + * @since 18.09.2018 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface TxConfigSupport { + + /** + * Note that bean will be obtained from guice context to be able to use injections. + * + * @return converter from annotation properties into common tx config object + */ + Class value(); +} diff --git a/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/unit/UnitManager.java b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/unit/UnitManager.java new file mode 100644 index 000000000..58353b2fc --- /dev/null +++ b/guicey-jdbi3/src/main/java/ru/vyarus/guicey/jdbi3/unit/UnitManager.java @@ -0,0 +1,89 @@ +package ru.vyarus.guicey.jdbi3.unit; + +import com.google.common.base.Preconditions; +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.Jdbi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Manages JDBI {@link Handle} for current unit of work. This handle must be used by all JDBI proxies. + * Unit of work is thread-bound (all actions in one thread participate in one unit of work). + * It is not intended to be used directly (only in really rare cases when manual unit required + * without opening transaction). + *

        + * Raw provider may be injected to obtain current handle: {@code @Inject Provider}. + * In all other cases transaction annotation must be used to wrap code into unit of work using guice aop. + * + * @author Vyacheslav Rusakov + * @see TransactionTemplate for manual transaction definition + * @since 31.08.2018 + */ +@Singleton +public class UnitManager implements Provider { + + private final Logger logger = LoggerFactory.getLogger(UnitManager.class); + + private final Jdbi jdbi; + private final ThreadLocal unit = new ThreadLocal<>(); + + /** + * Create a unit manager. + * + * @param jdbi jdbi instance + */ + @Inject + public UnitManager(final Jdbi jdbi) { + this.jdbi = jdbi; + } + + @Override + public Handle get() { + Preconditions.checkState(isUnitStarted(), "Unit of work not started yet"); + return unit.get(); + } + + /** + * @return true if unit of work started (and handle could be obtained), false otherwise + */ + public boolean isUnitStarted() { + return unit.get() != null; + } + + /** + * Starts unit of work. + * + * @throws IllegalStateException if unit of work already started + */ + public void beginUnit() { + Preconditions.checkState(!isUnitStarted(), "Unit of work already started"); + final Handle handle = jdbi.open(); + unit.set(handle); + logger.trace("Transaction start"); + } + + /** + * Finish unit of work. Note: does not commit transaction, but only close context handle. + * + * @throws IllegalStateException when no opened unit of work + */ + public void endUnit() { + Preconditions.checkState(isUnitStarted(), "Stop called outside of unit of work"); + final Handle handle = unit.get(); + // first remove handle to avoid stale handles in any case + unit.remove(); + try { + handle.close(); + } catch (Exception ex) { + // not entire stacktrace to avoid confusion: it may appear here only because of connection damage + // and so there will already be logged traces indicating connection problem + logger.warn("JDBI handle close error ({})", ex.getMessage()); + } + logger.trace("Transaction end"); + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractAppTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractAppTest.groovy new file mode 100644 index 000000000..93cf1103e --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractAppTest.groovy @@ -0,0 +1,12 @@ +package ru.vyarus.guicey.jdbi3 + +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.support.SampleApp + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@TestGuiceyApp(value = SampleApp, config = 'src/test/resources/test-config.yml') +abstract class AbstractAppTest extends AbstractTest { +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractTest.groovy new file mode 100644 index 000000000..7024961dd --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AbstractTest.groovy @@ -0,0 +1,29 @@ +package ru.vyarus.guicey.jdbi3 + +import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup +import ru.vyarus.guicey.jdbi3.support.db.FlywayInitBundle +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +abstract class AbstractTest extends Specification { + + static { + PropertyBundleLookup.enableBundles(FlywayInitBundle) + } + + @Inject + FlywayInitBundle.FlywaySupport flyway + + void setup() { + flyway.start() + } + + void cleanup() { + flyway.stop() + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AdvancedConfigurationTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AdvancedConfigurationTest.groovy new file mode 100644 index 000000000..7e7ddefb9 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/AdvancedConfigurationTest.groovy @@ -0,0 +1,54 @@ +package ru.vyarus.guicey.jdbi3 + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.jdbi.v3.core.array.SqlArrayArgumentStrategy +import org.jdbi.v3.core.array.SqlArrayTypes +import org.jdbi.v3.core.h2.H2DatabasePlugin +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.support.SampleApp +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 14.09.2018 + */ +@TestGuiceyApp(value = App, config = 'src/test/resources/test-config.yml') +class AdvancedConfigurationTest extends AbstractTest { + + @Inject + Bootstrap bootstrap + + def "Check custom configuration"() { + + expect: + (bootstrap.getApplication() as App).configCalled + } + + static class App extends Application { + + boolean configCalled = false + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(SampleApp.package.name) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database } + .withPlugins(new H2DatabasePlugin()) + .withConfig({ jdbi -> + // using block for plugin validation + assert jdbi.getConfig(SqlArrayTypes).getArgumentStrategy() == SqlArrayArgumentStrategy.OBJECT_ARRAY + configCalled = true + })) + .build()) + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingInstallationTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingInstallationTest.groovy new file mode 100644 index 000000000..fddfef6e1 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingInstallationTest.groovy @@ -0,0 +1,68 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.AbstractModule +import com.google.inject.CreationException +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.jdbi.v3.sqlobject.statement.SqlUpdate +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 29.10.2019 + */ +class BindingInstallationTest extends Specification { + def "Check incorrect repo definition detection"() { + + when: "staring app with incorrect repo declaration" + TestSupport.runCoreApp(App, 'src/test/resources/test-config.yml') + then: "error" + def ex = thrown(CreationException) + ex.getCause().getMessage() + .startsWith("JDBI repository BaseRepository can't be installed from binding: ") + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new AbstractModule() { + @Override + protected void configure() { + // suppose its some mocking attempt + bind(BaseRepository).toInstance(new BaseRepository() { + @Override + int insert(String name) { + return 0 + } + + @Override + int update(String name) { + return 0 + } + }) + } + }) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database }) + .build()); + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } + + @JdbiRepository + static interface BaseRepository { + @SqlUpdate("insert into table(name) values(:name)") + int insert(String name) + + @SqlUpdate("update table set name = :name") + int update(String name) + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest.groovy new file mode 100644 index 000000000..f42b0e3ee --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest.groovy @@ -0,0 +1,65 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.AbstractModule +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.jdbi.v3.sqlobject.statement.SqlQuery +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import ru.vyarus.guicey.jdbi3.support.mapper.SampleMapper +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 15.01.2022 + */ +@TestGuiceyApp(value = App, config = 'src/test/resources/test-config.yml') +class BindingRecognitionTest extends AbstractTest { + + @Inject + Repo repo + + def "Check correct repo definition detection"() { + + when: "trying to query repo" + repo.all() + then: "no error" + true + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .modules(new AbstractModule() { + @Override + protected void configure() { + // only right binding part is recognized as repo + bind(Repo).to(RepoImpl) + } + }) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database }) + .extensions(SampleMapper) + .build()); + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } + + static interface Repo { + @SqlQuery("select * from sample") + List all() + } + + @JdbiRepository + @InTransaction + static interface RepoImpl extends Repo {} +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest2.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest2.groovy new file mode 100644 index 000000000..b5a07bf97 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BindingRecognitionTest2.groovy @@ -0,0 +1,68 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.AbstractModule +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.jdbi.v3.sqlobject.statement.SqlQuery +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import ru.vyarus.guicey.jdbi3.support.mapper.SampleMapper +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 16.01.2022 + */ +@TestGuiceyApp(value = App, config = 'src/test/resources/test-config.yml') +class BindingRecognitionTest2 extends AbstractTest { + + @Inject + Repo repo + + def "Check correct repo definition detection"() { + + when: "trying to query repo" + repo.all() + then: "no error" + true + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + // recognition both as direct extension and from binding + .extensions(RepoImpl) + .modules(new AbstractModule() { + @Override + protected void configure() { + // only right binding part is recognized as repo + bind(Repo).to(RepoImpl) + } + }) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database }) + .extensions(SampleMapper) + .build()); + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } + + static interface Repo { + @SqlQuery("select * from sample") + List all() + } + + @JdbiRepository + @InTransaction + static interface RepoImpl extends Repo {} +} + diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BridgeMethodsCaseTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BridgeMethodsCaseTest.groovy new file mode 100644 index 000000000..b9c5ad4ba --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/BridgeMethodsCaseTest.groovy @@ -0,0 +1,30 @@ +package ru.vyarus.guicey.jdbi3 + +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.support.SampleEagerApp +import ru.vyarus.guicey.jdbi3.support.repository.syntetic.NamedEntity +import ru.vyarus.guicey.jdbi3.support.repository.syntetic.RootRepo + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 23.11.2022 + */ +@TestGuiceyApp(value = SampleEagerApp, config = 'src/test/resources/test-config.yml') +class BridgeMethodsCaseTest extends AbstractTest { + + @Inject + RootRepo repo + + def "Check methods resolution"() { + + when: "init record" + def entity = new NamedEntity(name: "test") + long id = repo.save(entity) + + then: "read saved" + def res = repo.get(id) + res.name == "test" + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CheckLazyProxies.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CheckLazyProxies.groovy new file mode 100644 index 000000000..1335ff520 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CheckLazyProxies.groovy @@ -0,0 +1,22 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.name.Named +import ru.vyarus.guicey.jdbi3.installer.repository.sql.SqlObjectProvider + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 23.06.2020 + */ +class CheckLazyProxies extends AbstractAppTest { + + @Inject @Named("jdbi3.proxies") + Set proxies + + def "Check proxies not initialized"() { + + expect: "no initialized proxies" + proxies.find {it.initialized} == null + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnMultiTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnMultiTest.groovy new file mode 100644 index 000000000..a46168de7 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnMultiTest.groovy @@ -0,0 +1,61 @@ +package ru.vyarus.guicey.jdbi3 + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.support.SampleApp +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import ru.vyarus.guicey.jdbi3.support.ann.CustTx +import ru.vyarus.guicey.jdbi3.support.repository.CustTxRepository +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@TestGuiceyApp(value = App, config = 'src/test/resources/test-config.yml') +class CustTxAnnMultiTest extends AbstractTest { + + @Inject + CustTxRepository custrepo + @Inject + SampleRepository repo + + def "Check def ann not work"() { + + when: "call in scope of old ann" + repo.all() + then: "ok" + true + } + + def "Check new ann scope"() { + + when: "call in scope of new ann" + custrepo.all() + then: "ok" + true + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(SampleApp.package.name) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database } + .withTxAnnotations(CustTx, InTransaction)) + .build()) + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnTest.groovy new file mode 100644 index 000000000..714af41b5 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/CustTxAnnTest.groovy @@ -0,0 +1,61 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.ProvisionException +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.support.SampleApp +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import ru.vyarus.guicey.jdbi3.support.ann.CustTx +import ru.vyarus.guicey.jdbi3.support.repository.CustTxRepository +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@TestGuiceyApp(value = App, config = 'src/test/resources/test-config.yml') +class CustTxAnnTest extends AbstractTest { + + @Inject + CustTxRepository custrepo + @Inject + SampleRepository repo + + def "Check def ann not work"() { + + when: "call in scope of old ann" + repo.all() + then: "no unit" + thrown(ProvisionException) + } + + def "Check new ann scope"() { + + when: "call in scope of new ann" + custrepo.all() + then: "ok" + true + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(SampleApp.package.name) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database } + .withTxAnnotations(CustTx)) + .build()) + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/EagerInitTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/EagerInitTest.groovy new file mode 100644 index 000000000..3b2f78060 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/EagerInitTest.groovy @@ -0,0 +1,32 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.name.Named +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.jdbi3.installer.repository.sql.SqlObjectProvider +import ru.vyarus.guicey.jdbi3.support.SampleEagerApp +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 23.06.2020 + */ +@TestGuiceyApp(value = SampleEagerApp, config = 'src/test/resources/test-config.yml') +class EagerInitTest extends AbstractTest { + + @Inject @Named("jdbi3.proxies") + Set proxies + + @Inject + SampleRepository repository + + def "Check eager proxies correctness"() { + + expect: "all proxies initialized" + proxies.find { !it.initialized } == null + + and: "repository works" + repository.all().empty + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/IncompatibleTxConfigTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/IncompatibleTxConfigTest.groovy new file mode 100644 index 000000000..86f9a2d7e --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/IncompatibleTxConfigTest.groovy @@ -0,0 +1,51 @@ +package ru.vyarus.guicey.jdbi3 + +import org.jdbi.v3.core.transaction.TransactionException +import org.jdbi.v3.core.transaction.TransactionIsolationLevel +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 28.09.2018 + */ +class IncompatibleTxConfigTest extends AbstractAppTest { + + @Inject + TxService service + + def "Check tx configuration errors"() { + + when: "tx level change" + service.levelErr() + then: + def ex = thrown(TransactionException) + ex.message == "Tried to execute nested @Transaction(READ_UNCOMMITTED), but already running in a transaction with isolation level READ_COMMITTED." + + when: "readonly change" + service.readOnlyErr() + then: + true // error not thrown because h2 ignores readonly flag! + } + + @InTransaction + static class TxService { + + void levelErr(){ + custLevelCall() + } + + @InTransaction(readOnly = true) + void readOnlyErr() { + nonReadOnly() + } + + @InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED) + void custLevelCall() { + } + + void nonReadOnly() { + } + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InjectionGetterTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InjectionGetterTest.groovy new file mode 100644 index 000000000..1b48bc03e --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InjectionGetterTest.groovy @@ -0,0 +1,37 @@ +package ru.vyarus.guicey.jdbi3 + +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.support.repository.CustTxRepository +import ru.vyarus.guicey.jdbi3.support.repository.LogicfulRepository +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 17.09.2018 + */ +class InjectionGetterTest extends AbstractAppTest { + + @Inject + SampleRepository repo + + @Inject + LogicfulRepository repo2 + + def "Check injector getter"() { + + expect: "getter works" + repo.custRepo instanceof CustTxRepository + } + + def "Check default method"() { + + when: "filling repo" + repo.save(new Sample(name: "test1")) + repo.save(new Sample(name: "test2")) + + then: "access through injection works" + repo2.checkInject().size() == 2 + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InvalidRepoDeclarationTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InvalidRepoDeclarationTest.groovy new file mode 100644 index 000000000..1fa91a429 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/InvalidRepoDeclarationTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.guicey.jdbi3 + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.jdbi.v3.sqlobject.statement.SqlUpdate +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 29.10.2019 + */ +class InvalidRepoDeclarationTest extends Specification { + + def "Check incorrect repo definition detection"() { + + when: "staring app with incorrect repo declaration" + TestSupport.runCoreApp(App, 'src/test/resources/test-config.yml') + then: "error" + def ex = thrown(IllegalStateException) + ex.getMessage() == "Incorrect repository BaseRepository declaration: base interface CrudRepository is also annotated with @JdbiRepository which may break AOP mappings. Only root repository class must be annotated." + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(BaseRepository) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database }) + .build()); + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } + } + + @JdbiRepository + static interface CrudRepository { + int insert(String name); + + int update(String name); + } + + @JdbiRepository + static interface BaseRepository extends CrudRepository { + @SqlUpdate("insert into table(name) values(:name)") + int insert(String name); + + @SqlUpdate("update table set name = :name") + int update(String name); + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/MappingTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/MappingTest.groovy new file mode 100644 index 000000000..107ecb967 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/MappingTest.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.Inject +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class MappingTest extends AbstractAppTest { + + @Inject + SampleRepository repository + + def "Check repository and mapper works"() { + + when: "saving records" + repository.save(new Sample(name: "test")) + + then: "read saved" + def res = repository.all() + res.size() == 1 + res[0].name == "test" + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxConfigurationTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxConfigurationTest.groovy new file mode 100644 index 000000000..3fa6f38f9 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxConfigurationTest.groovy @@ -0,0 +1,55 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.Provider +import org.jdbi.v3.core.Handle +import org.jdbi.v3.core.transaction.TransactionIsolationLevel +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +/** + * @author Vyacheslav Rusakov + * @since 28.09.2018 + */ +class TxConfigurationTest extends AbstractAppTest { + + @Inject + TxService service + + def "Check tx configuration appliance"() { + + expect: + service.defLevelCall() == TransactionIsolationLevel.READ_COMMITTED + service.custLevelCall() == TransactionIsolationLevel.READ_UNCOMMITTED + !service.defReadOnly() + !service.readOnly() // h2 ignores this flag + } + + @Singleton + static class TxService { + + @Inject + Provider handle + + @InTransaction() + TransactionIsolationLevel defLevelCall() { + return handle.get().getTransactionIsolationLevel() + } + + @InTransaction(TransactionIsolationLevel.READ_UNCOMMITTED) + TransactionIsolationLevel custLevelCall() { + return handle.get().getTransactionIsolationLevel() + } + + @InTransaction() + boolean defReadOnly() { + return handle.get().isReadOnly() + } + + @InTransaction(readOnly = true) + boolean readOnly() { + return handle.get().isReadOnly() + } + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxTest.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxTest.groovy new file mode 100644 index 000000000..6eafbcf7a --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/TxTest.groovy @@ -0,0 +1,100 @@ +package ru.vyarus.guicey.jdbi3 + +import com.google.inject.Provider +import com.google.inject.ProvisionException +import org.jdbi.v3.core.Handle +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.support.repository.CustTxRepository +import ru.vyarus.guicey.jdbi3.support.repository.SampleRepository +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class TxTest extends AbstractAppTest { + + @Inject + CustTxRepository notxrepo + @Inject + SampleRepository repo + @Inject + Provider handle + @Inject + TransactionTemplate template + + def "Check handle access"() { + + when: 'get handle outside of tx' + handle.get() + then: "err" + thrown(ProvisionException) + + when: 'accessing inside tx' + template.inTransaction { handle.get() } + then: "ok" + true + } + + def "Check dao access"() { + + when: "accessing dao without tx" + notxrepo.all() + then: 'err' + thrown(ProvisionException) + + when: "accessing dao in tx" + template.inTransaction { notxrepo.all() } + then: "ok" + true + + when: "accessing annotated dao" + repo.all() + then: "ok" + true + } + + def "Check nested tx"() { + + when: "call annotated repo inside tx" + template.inTransaction({ + notxrepo.save(new Sample(name: 'test')) + // nested tx + assert repo.all().size() == 1 + }) + then: "ok" + true + + } + + def "Check rollback"() { + + when: "fail tx" + template.inTransaction({ + notxrepo.save(new Sample(name: 'test')) + throw new IllegalStateException("ups") + }) + then: "ex propagated and state rolled back" + thrown(IllegalStateException) + repo.all().isEmpty() + + } + + def "Check nested tx rollback"() { + + when: 'exception in nested' + template.inTransaction({ + notxrepo.save(new Sample(name: 'test')) + template.inTransaction({ + notxrepo.save(new Sample(name: 'test2')) + assert notxrepo.all().size() == 2 + throw new IllegalStateException("ups") + }) + }) + then: "ex propagated and state rolled back" + thrown(IllegalStateException) + repo.all().isEmpty() + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleApp.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleApp.groovy new file mode 100644 index 000000000..ab9062b36 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleApp.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.jdbi3.support + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.jdbi3.JdbiBundle + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class SampleApp extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(SampleApp.package.name) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database }) + .build()) + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleConfiguration.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleConfiguration.groovy new file mode 100644 index 000000000..9b8535d8d --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleConfiguration.groovy @@ -0,0 +1,18 @@ +package ru.vyarus.guicey.jdbi3.support + +import io.dropwizard.core.Configuration +import io.dropwizard.db.DataSourceFactory + +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class SampleConfiguration extends Configuration { + + @Valid + @NotNull + DataSourceFactory database = new DataSourceFactory(); +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleEagerApp.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleEagerApp.groovy new file mode 100644 index 000000000..238b2386d --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/SampleEagerApp.groovy @@ -0,0 +1,27 @@ +package ru.vyarus.guicey.jdbi3.support + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.jdbi3.JdbiBundle + +/** + * @author Vyacheslav Rusakov + * @since 23.06.2020 + */ +class SampleEagerApp extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig(SampleApp.package.name) + .bundles(JdbiBundle. forDatabase { conf, env -> conf.database } + .withEagerInitialization()) + .build()) + } + + @Override + void run(SampleConfiguration configuration, Environment environment) throws Exception { + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/ann/CustTx.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/ann/CustTx.groovy new file mode 100644 index 000000000..9efe0492d --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/ann/CustTx.groovy @@ -0,0 +1,16 @@ +package ru.vyarus.guicey.jdbi3.support.ann + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target([ElementType.TYPE, ElementType.METHOD]) +@interface CustTx { + +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/db/FlywayInitBundle.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/db/FlywayInitBundle.groovy new file mode 100644 index 000000000..bf080d46c --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/db/FlywayInitBundle.groovy @@ -0,0 +1,51 @@ +package ru.vyarus.guicey.jdbi3.support.db + +import io.dropwizard.db.DataSourceFactory +import io.dropwizard.lifecycle.Managed +import org.flywaydb.core.Flyway +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.module.installer.order.Order +import ru.vyarus.guicey.jdbi3.support.SampleConfiguration + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class FlywayInitBundle implements GuiceyBundle { + + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.extensions(FlywaySupport) + } + + @Order(Integer.MIN_VALUE) + static class FlywaySupport implements Managed { + + @Inject + SampleConfiguration conf + Flyway flyway + + @Override + void start() throws Exception { + if (flyway != null) { + return + } + DataSourceFactory f = conf.getDatabase(); + flyway = Flyway.configure().cleanDisabled(false) + .dataSource(f.getUrl(), f.getUser(), f.getPassword()) + .load(); + flyway.migrate(); + } + + @Override + void stop() throws Exception { + if (flyway != null) { + flyway.clean() + flyway = null + } + } + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/SampleMapper.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/SampleMapper.groovy new file mode 100644 index 000000000..ed4a1de8b --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/SampleMapper.groovy @@ -0,0 +1,21 @@ +package ru.vyarus.guicey.jdbi3.support.mapper + +import org.jdbi.v3.core.mapper.RowMapper +import org.jdbi.v3.core.statement.StatementContext +import ru.vyarus.guicey.jdbi3.support.model.Sample + +import java.sql.ResultSet +import java.sql.SQLException + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class SampleMapper implements RowMapper { + + @Override + Sample map(ResultSet rs, StatementContext ctx) throws SQLException { + return new Sample(id: rs.getLong("id"), + name: rs.getString("name")) + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/binder/SampleBind.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/binder/SampleBind.groovy new file mode 100644 index 000000000..8ad0ee892 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/mapper/binder/SampleBind.groovy @@ -0,0 +1,41 @@ +package ru.vyarus.guicey.jdbi3.support.mapper.binder + + +import org.jdbi.v3.core.statement.SqlStatement +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizingAnnotation +import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer +import ru.vyarus.guicey.jdbi3.support.model.Sample + +import java.lang.annotation.* +import java.lang.reflect.Method +import java.lang.reflect.Parameter +import java.lang.reflect.Type + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@SqlStatementCustomizingAnnotation(SampleBind.SampleBinder.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@interface SampleBind { + + static class SampleBinder implements SqlStatementCustomizerFactory { + + + @Override + public SqlStatementParameterCustomizer createForParameter(Annotation annotation, + Class sqlObjectType, + Method method, + Parameter param, + int index, + Type type) { + { stmt, sample -> + ((SqlStatement) stmt) + .bind("id", ((Sample) sample).id) + .bind("name", ((Sample) sample).name) + } + } + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/model/Sample.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/model/Sample.groovy new file mode 100644 index 000000000..cf36e7321 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/model/Sample.groovy @@ -0,0 +1,11 @@ +package ru.vyarus.guicey.jdbi3.support.model + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class Sample { + + long id + String name +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/CustTxRepository.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/CustTxRepository.groovy new file mode 100644 index 000000000..1665afcdb --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/CustTxRepository.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.guicey.jdbi3.support.repository + +import org.jdbi.v3.sqlobject.statement.SqlQuery +import org.jdbi.v3.sqlobject.statement.SqlUpdate +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.ann.CustTx +import ru.vyarus.guicey.jdbi3.support.mapper.binder.SampleBind +import ru.vyarus.guicey.jdbi3.support.model.Sample + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@JdbiRepository +@CustTx +interface CustTxRepository { + + @SqlQuery("select * from sample") + List all() + + @SqlUpdate("insert into sample (name) values (:name)") + void save(@SampleBind Sample sample) + +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/LogicfulRepository.java b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/LogicfulRepository.java new file mode 100644 index 000000000..e3c6f2ffc --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/LogicfulRepository.java @@ -0,0 +1,32 @@ +package ru.vyarus.guicey.jdbi3.support.repository; + +import com.google.common.base.Preconditions; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository; +import ru.vyarus.guicey.jdbi3.support.model.Sample; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; + +import jakarta.inject.Inject; +import java.util.List; + +/** + * @author Vyacheslav Rusakov + * @since 17.09.2018 + */ +@JdbiRepository +@InTransaction +public interface LogicfulRepository { + + @SqlQuery("select * from sample") + List all(); + + @Inject + CustTxRepository getCustRepo(); + + default List checkInject() { + List all = all(); + List all2 = getCustRepo().all(); + Preconditions.checkState(all.size() == all2.size()); + return all2; + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/SampleRepository.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/SampleRepository.groovy new file mode 100644 index 000000000..e7c8bd52c --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/SampleRepository.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.jdbi3.support.repository + +import org.jdbi.v3.sqlobject.statement.SqlQuery +import org.jdbi.v3.sqlobject.statement.SqlUpdate +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository +import ru.vyarus.guicey.jdbi3.support.mapper.binder.SampleBind +import ru.vyarus.guicey.jdbi3.support.model.Sample +import ru.vyarus.guicey.jdbi3.tx.InTransaction + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@JdbiRepository +@InTransaction +interface SampleRepository { + + @SqlQuery("select * from sample") + List all() + + @SqlUpdate("insert into sample (name) values (:name)") + void save(@SampleBind Sample sample) + + @Inject + CustTxRepository getCustRepo(); +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/BaseRepo.java b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/BaseRepo.java new file mode 100644 index 000000000..c2f47f54e --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/BaseRepo.java @@ -0,0 +1,12 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic; + +/** + * @author Vyacheslav Rusakov + * @since 23.11.2022 + */ +public interface BaseRepo { + + long save(T sample); + + T get(long id); +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/IdEntity.java b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/IdEntity.java new file mode 100644 index 000000000..a4b272d83 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/IdEntity.java @@ -0,0 +1,17 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic; + +/** + * @author Vyacheslav Rusakov + * @since 24.11.2022 + */ +public class IdEntity { + private long id; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedBind.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedBind.groovy new file mode 100644 index 000000000..196be3add --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedBind.groovy @@ -0,0 +1,39 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic + +import org.jdbi.v3.core.statement.SqlStatement +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizingAnnotation +import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer + +import java.lang.annotation.* +import java.lang.reflect.Method +import java.lang.reflect.Parameter +import java.lang.reflect.Type + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +@SqlStatementCustomizingAnnotation(NamedBinder.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@interface NamedBind { + + static class NamedBinder implements SqlStatementCustomizerFactory { + + + @Override + public SqlStatementParameterCustomizer createForParameter(Annotation annotation, + Class sqlObjectType, + Method method, + Parameter param, + int index, + Type type) { + { stmt, sample -> + ((SqlStatement) stmt) + .bind("id", ((NamedEntity) sample).id) + .bind("name", ((NamedEntity) sample).name) + } + } + } +} \ No newline at end of file diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntMapper.groovy b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntMapper.groovy new file mode 100644 index 000000000..7aa5def4f --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntMapper.groovy @@ -0,0 +1,20 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic + +import org.jdbi.v3.core.mapper.RowMapper +import org.jdbi.v3.core.statement.StatementContext + +import java.sql.ResultSet +import java.sql.SQLException + +/** + * @author Vyacheslav Rusakov + * @since 31.08.2018 + */ +class NamedEntMapper implements RowMapper { + + @Override + NamedEntity map(ResultSet rs, StatementContext ctx) throws SQLException { + return new NamedEntity(id: rs.getLong("id"), + name: rs.getString("name")) + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntity.java b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntity.java new file mode 100644 index 000000000..8fd0fa337 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/NamedEntity.java @@ -0,0 +1,17 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic; + +/** + * @author Vyacheslav Rusakov + * @since 24.11.2022 + */ +public class NamedEntity extends IdEntity { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/RootRepo.java b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/RootRepo.java new file mode 100644 index 000000000..4e79ac674 --- /dev/null +++ b/guicey-jdbi3/src/test/groovy/ru/vyarus/guicey/jdbi3/support/repository/syntetic/RootRepo.java @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.jdbi3.support.repository.syntetic; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import ru.vyarus.guicey.jdbi3.installer.repository.JdbiRepository; +import ru.vyarus.guicey.jdbi3.tx.InTransaction; + +/** + * @author Vyacheslav Rusakov + * @since 23.11.2022 + */ +@JdbiRepository +@InTransaction +public interface RootRepo extends BaseRepo { + + @Override + @SqlUpdate("insert into sample (name) values (:name)") + @GetGeneratedKeys + long save(@NamedBind NamedEntity sample); + + @Override + @SqlQuery("select * from sample where id=:id") + NamedEntity get(@Bind("id") long id); +} diff --git a/guicey-jdbi3/src/test/resources/db/migration/V1__setup.sql b/guicey-jdbi3/src/test/resources/db/migration/V1__setup.sql new file mode 100644 index 000000000..9bbc5038b --- /dev/null +++ b/guicey-jdbi3/src/test/resources/db/migration/V1__setup.sql @@ -0,0 +1,5 @@ +CREATE TABLE sample ( + id IDENTITY NOT NULL, + name VARCHAR, + CONSTRAINT sample_id PRIMARY KEY (id) +); \ No newline at end of file diff --git a/guicey-jdbi3/src/test/resources/test-config.yml b/guicey-jdbi3/src/test/resources/test-config.yml new file mode 100644 index 000000000..9c53f80c4 --- /dev/null +++ b/guicey-jdbi3/src/test/resources/test-config.yml @@ -0,0 +1,15 @@ +database: + driverClass: org.h2.Driver + user: sa + password: + url: jdbc:h2:mem:test + +server: + rootPath: '/rest/*' + + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 diff --git a/guicey-lifecycle-annotations/README.md b/guicey-lifecycle-annotations/README.md new file mode 100644 index 000000000..848f729ac --- /dev/null +++ b/guicey-lifecycle-annotations/README.md @@ -0,0 +1,98 @@ +# Lifecycle annotations + +### About + +Allows using lifecycle annotations for initialization/destruction methods in guice beans. +Main motivation is to replace `Managed` usage in places where it's simpler to just annotate method, rather than +register extension. + +* `@PostCostruct` - same as `Managed.start()` +* `@PostStartup` - called after server startup (application completely started) +* `@PreDestroy` - same as `Managed.stop()` + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-lifecycle-annotations + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-lifecycle-annotations:{guicey.version}' +``` + +Omit version if guicey BOM used. + + +### Usage + +By default no setup required: bundle will be loaded automatically with the bundles lookup mechanism (enabled by default). +So just add jar into classpath and annotations will work. + +```java +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import ru.vyarus.guicey.annotations.lifecycle.PostStartup; + +public class SampleBean { + + @PostConstruct + private void start() { + // same time as Managed.start() + } + + @PostStartup + private void afterStartup() { + // application completely started + } + + @PreDestroy + private void stop() { + // same time as Managed.stop() + } +} +``` + +* Annotated methods must not contain parameters. Method could have any visibility. +* `@PostConstruct` or `@PostStartup` methods fail fails entire application startup (fail fast) +* `@PreDestroy` method fails are just logged to guarantee that all destroy methods will be procesed +* If both current class and super class have annotated methods - both methods will be executed (the only obvious exception is overridden methods) + +IMPORTANT: if bean is created on demand (lazy creation by guice JIT), annotated methods will still be called, +even if actual lifecycle event was already passed. Warning log message will be printed to indicate this "not quite correct" execution, +but you can be sure that your methods will always be processed. + +#### Reducing scope + +Annotations are applied using guice [TypeListener api](http://google.github.io/guice/api-docs/latest/javadoc/index.html?com/google/inject/spi/TypeListener.html) +which means that all guice beans are introspected for annotated methods. + +If you want to limit the scope of processed beans then register bundle manually +(in this case lookup will be ignored): + +```java +GuiceBundle.builder() + .bundles(new LifecycleAnnotationsBundle("package.to.apply")) + .build() +``` + +In this example only beans lying in specified package will be checked. + +Also, direct `Matcher` implementation could be specified for more sophisticated cases. +For example, if I want to exclude only one class: + +```java +new LifecycleAnnotationsBundle(new AbstractMatcher>() { + @Override + public boolean matches(TypeLiteral o) { + return o.getRawType() != SomeExcludedBean.class; + } + }) +``` \ No newline at end of file diff --git a/guicey-lifecycle-annotations/build.gradle b/guicey-lifecycle-annotations/build.gradle new file mode 100644 index 000000000..bd40b831c --- /dev/null +++ b/guicey-lifecycle-annotations/build.gradle @@ -0,0 +1,7 @@ +description = "Dropwizard lifecycle annotations" + +dependencies { + implementation ('ru.vyarus:guice-ext-annotations') { + exclude group: 'com.google.inject', module: 'guice' + } +} \ No newline at end of file diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/LifecycleAnnotationsBundle.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/LifecycleAnnotationsBundle.java new file mode 100644 index 000000000..b65003759 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/LifecycleAnnotationsBundle.java @@ -0,0 +1,82 @@ +package ru.vyarus.guicey.annotations.lifecycle; + +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.guice.ext.core.util.ObjectPackageMatcher; +import ru.vyarus.guicey.annotations.lifecycle.module.DropwizardLifecycleListener; +import ru.vyarus.guicey.annotations.lifecycle.module.LifecycleAnnotationsModule; + +/** + * Bundle enabled usage of lifecycle annotations in guice beans. Supported annotations: + *

          + *
        • {@link jakarta.annotation.PostConstruct} - same as {@link io.dropwizard.lifecycle.Managed#start()}
        • + *
        • {@link PostStartup} - called after server startup + * (dropwizard {@link io.dropwizard.lifecycle.ServerLifecycleListener} used)
        • + *
        • {@link jakarta.annotation.PreDestroy} - same as {@link io.dropwizard.lifecycle.Managed#stop()}
        • + *
        + *

        + * The main intention is to replace usages of {@link io.dropwizard.lifecycle.Managed} beans with annotations, because + * registration of managed beans requires classpath scan usage or manual extensions registration. + *

        + * Annotations may be applied to any method without arguments. Annotations in super classes would also be detected. + * One method could have multiple annotations. Multiple methods could be annotated with the same annotation. + *

        + * If bean is registered after events processing (lazy bean, created by JIT) then event methods will be process + * immediately after bean creation (with warnings in log). + *

        + * Bundle is installed automatically using guicey bundles lookup. If you need to limit the scope of beans to search + * annotations on then register bundle directly with declared custom matcher. For example: + *

        {@code
        + *      builder.bundles(new LifecycleAnnotationsBundle("package.to.apply"))
        + * }
        + * (only one instance of bundle will be used) + * + * @author Vyacheslav Rusakov + * @since 08.11.2018 + */ +public class LifecycleAnnotationsBundle extends UniqueGuiceyBundle { + + private final Matcher> typeMatcher; + + /** + * Default module constructor to check annotations on all beans. + */ + public LifecycleAnnotationsBundle() { + this(Matchers.any()); + } + + /** + * Constructs annotation module with annotation scan limited to provided package. + * (used mainly for startup performance optimization) + * + * @param pkg package to limit beans, where annotations processed + */ + public LifecycleAnnotationsBundle(final String pkg) { + this(new ObjectPackageMatcher<>(pkg)); + } + + /** + * Constructs annotation module with custom bean matcher for annotations processing. + * + * @param typeMatcher matcher to select beans for annotations processing + * @see ObjectPackageMatcher as example matcher implementation + */ + public LifecycleAnnotationsBundle(final Matcher> typeMatcher) { + this.typeMatcher = typeMatcher; + } + + @Override + public void run(final GuiceyEnvironment environment) { + final LifecycleAnnotationsModule module = new LifecycleAnnotationsModule(typeMatcher); + final DropwizardLifecycleListener lifecycle = new DropwizardLifecycleListener(module.getCollector()); + + environment + .modules(module) + // do not register as extension to not put additional beans into the guice context + .manage(lifecycle) + .listenServer(lifecycle); + } +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/PostStartup.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/PostStartup.java new file mode 100644 index 000000000..12c3da585 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/PostStartup.java @@ -0,0 +1,26 @@ +package ru.vyarus.guicey.annotations.lifecycle; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation for marking initialization methods on guice beans to be called after server startup. Note that + * this method will not be called under guicey lightweight test or environment command start. + *

        + * In contrast to {@link jakarta.annotation.PostConstruct} which is called on + * {@link io.dropwizard.lifecycle.Managed#start()} (during server initialization), + * annotated methods are called only after complete server startup (when application is ready to serve requests). + * + * @author Vyacheslav Rusakov + * @since 08.11.2018 + * @see LifecycleAnnotationsBundle + */ +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface PostStartup { +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/DropwizardLifecycleListener.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/DropwizardLifecycleListener.java new file mode 100644 index 000000000..8efcafc74 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/DropwizardLifecycleListener.java @@ -0,0 +1,45 @@ +package ru.vyarus.guicey.annotations.lifecycle.module; + +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.lifecycle.ServerLifecycleListener; +import org.eclipse.jetty.server.Server; +import ru.vyarus.guicey.annotations.lifecycle.PostStartup; +import ru.vyarus.guicey.annotations.lifecycle.module.collector.MethodsCollector; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +/** + * Listener bean used to process annotated methods in appropriate dropwizard lifecycle phases. + * + * @author Vyacheslav Rusakov + * @since 26.11.2018 + */ +public class DropwizardLifecycleListener implements ServerLifecycleListener, Managed { + + private final MethodsCollector collector; + + /** + * Create lifecycle listener. + * + * @param collector lifecycle methods registry + */ + public DropwizardLifecycleListener(final MethodsCollector collector) { + this.collector = collector; + } + + @Override + public void start() throws Exception { + collector.call(PostConstruct.class); + } + + @Override + public void serverStarted(final Server server) { + collector.call(PostStartup.class); + } + + @Override + public void stop() throws Exception { + collector.safeCall(PreDestroy.class); + } +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/LifecycleAnnotationsModule.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/LifecycleAnnotationsModule.java new file mode 100644 index 000000000..a50c6a4ab --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/LifecycleAnnotationsModule.java @@ -0,0 +1,68 @@ +package ru.vyarus.guicey.annotations.lifecycle.module; + +import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.Matcher; +import ru.vyarus.guice.ext.core.method.AnnotatedMethodTypeListener; +import ru.vyarus.guicey.annotations.lifecycle.PostStartup; +import ru.vyarus.guicey.annotations.lifecycle.module.collector.MethodsCollector; +import ru.vyarus.guicey.annotations.lifecycle.module.collector.SimpleAnnotationProcessor; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +import java.lang.annotation.Annotation; + +/** + * Guice module detects all methods annotated with lifecycle annotations. Annotations triggering done by + * {@link DropwizardLifecycleListener}. + * + * @author Vyacheslav Rusakov + * @see ru.vyarus.guicey.annotations.lifecycle.LifecycleAnnotationsBundle + * @since 26.11.2018 + */ +public class LifecycleAnnotationsModule extends AbstractModule { + + private final Matcher> typeMatcher; + private final MethodsCollector collector = new MethodsCollector(); + + /** + * Create lifecycle annotations module. + * + * @param typeMatcher target types matcher + */ + public LifecycleAnnotationsModule(final Matcher> typeMatcher) { + this.typeMatcher = typeMatcher; + } + + /** + * Used for triggering logic. + * + * @return collector instance + */ + public MethodsCollector getCollector() { + return collector; + } + + @Override + protected void configure() { + register(collector, + PostConstruct.class, + PostStartup.class, + PreDestroy.class + ); + } + + /** + * @param collector collector instance + * @param annotations annotation types to search in beans + */ + @SafeVarargs + private final void register(final MethodsCollector collector, + final Class... annotations) { + for (Class ann : annotations) { + bindListener(typeMatcher, new AnnotatedMethodTypeListener<>( + ann, new SimpleAnnotationProcessor<>(collector))); + } + } +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodInstance.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodInstance.java new file mode 100644 index 000000000..481d442cd --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodInstance.java @@ -0,0 +1,50 @@ +package ru.vyarus.guicey.annotations.lifecycle.module.collector; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; + +/** + * Instance method abstraction. Holds both method and target instance object to easily perform call. + * + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +public class MethodInstance { + private final Logger logger = LoggerFactory.getLogger(MethodInstance.class); + + private final Object instance; + private final Method method; + + /** + * Create a method instance hodler. + * + * @param instance object instance + * @param method target method + */ + public MethodInstance(final Object instance, final Method method) { + this.instance = instance; + this.method = method; + } + + /** + * Calls method on instance. + *

        + * If method execution fails, exception is propagated. + */ + public void call() { + try { + logger.debug("Executing method {}", this); + method.invoke(instance); + } catch (Exception ex) { + throw new IllegalStateException("Failed to execute method " + this, ex); + } + } + + @Override + public String toString() { + return instance.getClass().getSimpleName() + "." + method.getName() + + " of instance " + instance.toString(); + } +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodsCollector.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodsCollector.java new file mode 100644 index 000000000..871acd032 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/MethodsCollector.java @@ -0,0 +1,103 @@ +package ru.vyarus.guicey.annotations.lifecycle.module.collector; + +import com.google.common.base.Preconditions; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Registry for detected annotated methods. Used to collect and then process all found methods by annotation. + * + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +public class MethodsCollector { + private final Logger logger = LoggerFactory.getLogger(MethodsCollector.class); + + private final Multimap, MethodInstance> listeners = LinkedListMultimap.create(); + + // first it prevents duplicate lifecycle call + // second it used to detect late registrations for immediate execution + private final List> processed = new ArrayList<>(); + + /** + * Register lifecycle method. + * + * @param annotation lifecycle annotation + * @param instance object instance + * @param method annotated method + */ + public void register(final Class annotation, + final Object instance, + final Method method) { + final MethodInstance methodInstance = new MethodInstance(instance, method); + listeners.put(annotation, methodInstance); + // could appear due to JIT (when bean not registered and being instantiated after injector creation (on demand)) + if (processed.contains(annotation)) { + logger.warn("@{} listener registered after event processing: {}. " + + "This could happen when bean is not registered and instantiated on demand " + + "(by guice JIT). " + + "To avoid this warning register bean directly in guice module. " + + "Immediate initialization will be performed.", annotation.getSimpleName(), methodInstance); + + callInstance(annotation, methodInstance, true); + } + } + + /** + * Called to process all methods annotated with provided annotation. + * In case of exception, it would be propagated. + * + * @param annotation target method annotation + */ + public void call(final Class annotation) { + doCall(annotation, false); + } + + /** + * Called to process all methods annotated with provided annotation. + * If method execution fails, the exception is just logged without propagation. So all annotated methods would + * be called. + * + * @param annotation target method annotation + */ + public void safeCall(final Class annotation) { + doCall(annotation, true); + } + + private void doCall(final Class annotation, final boolean safe) { + Preconditions.checkState(!processed.contains(annotation), + "Lifecycle @%s methods were already processed", annotation.getSimpleName()); + processed.add(annotation); + + final Collection methods = listeners.get(annotation); + if (!methods.isEmpty()) { + logger.debug("Executing @{} lifecycle methods", annotation.getSimpleName()); + for (MethodInstance method : methods) { + callInstance(annotation, method, safe); + } + } + } + + private void callInstance(final Class annotation, + final MethodInstance method, + final boolean safe) { + try { + method.call(); + } catch (Exception ex) { + if (safe) { + // method name would be contained in the exception + logger.error("Failed to process @" + annotation.getSimpleName() + " annotated method", ex); + } else { + throw ex; + } + } + } +} diff --git a/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/SimpleAnnotationProcessor.java b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/SimpleAnnotationProcessor.java new file mode 100644 index 000000000..b2af14bc8 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/java/ru/vyarus/guicey/annotations/lifecycle/module/collector/SimpleAnnotationProcessor.java @@ -0,0 +1,34 @@ +package ru.vyarus.guicey.annotations.lifecycle.module.collector; + +import ru.vyarus.guice.ext.core.method.MethodPostProcessor; +import ru.vyarus.guice.ext.core.util.Utils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * Post processor implementation for registration of all found annotated methods inside collector. + * + * @param annotation type + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +public class SimpleAnnotationProcessor implements MethodPostProcessor { + + private final MethodsCollector collector; + + /** + * Create annotation processor. + * + * @param collector annotated methods collector + */ + public SimpleAnnotationProcessor(final MethodsCollector collector) { + this.collector = collector; + } + + @Override + public void process(final T annotation, final Method method, final Object instance) throws Exception { + Utils.checkNoParams(method); + collector.register(annotation.annotationType(), instance, method); + } +} diff --git a/guicey-lifecycle-annotations/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle b/guicey-lifecycle-annotations/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle new file mode 100644 index 000000000..b1b1852c4 --- /dev/null +++ b/guicey-lifecycle-annotations/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle @@ -0,0 +1 @@ +ru.vyarus.guicey.annotations.lifecycle.LifecycleAnnotationsBundle \ No newline at end of file diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/CollectorTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/CollectorTest.groovy new file mode 100644 index 000000000..27d4d379d --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/CollectorTest.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import ru.vyarus.guicey.annotations.lifecycle.module.collector.MethodsCollector +import spock.lang.Specification + +import jakarta.annotation.PostConstruct + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class CollectorTest extends Specification { + + def "Check duplicate events prevention"() { + + when: + MethodsCollector collector = new MethodsCollector() + collector.call(PostConstruct) + collector.call(PostConstruct) + then: + def ex = thrown(IllegalStateException) + ex.message == "Lifecycle @PostConstruct methods were already processed" + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/ConfigurationTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/ConfigurationTest.groovy new file mode 100644 index 000000000..02d1a1501 --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/ConfigurationTest.groovy @@ -0,0 +1,109 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import com.google.inject.TypeLiteral +import com.google.inject.matcher.AbstractMatcher +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import spock.lang.Specification + +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy +import jakarta.inject.Singleton + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class ConfigurationTest extends Specification { + + def "Check bundle configuration"() { + + when: + SampleBean bean + SampleBean2 bean2 + TestSupport.runWebApp(App, null) { + bean = it.getInstance(SampleBean) + bean2 = it.getInstance(SampleBean2) + } + + then: + !bean.initialized + !bean.started + !bean.destroyed + + and: + bean2.initialized + bean2.started + bean2.destroyed + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder() + // SampleBean will not be processed + .bundles(new LifecycleAnnotationsBundle(new AbstractMatcher>() { + + @Override + boolean matches(TypeLiteral o) { + return o.getRawType() != SampleBean + } + })) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Singleton + static class SampleBean { + boolean initialized + boolean started + boolean destroyed + + @PostConstruct + private void start() { + initialized = true + } + + @PostStartup + private void afterStartup() { + started = true + } + + @PreDestroy + private void stop() { + destroyed = true + } + } + + @Singleton + static class SampleBean2 { + boolean initialized + boolean started + boolean destroyed + + @PostConstruct + private void start() { + initialized = true + } + + @PostStartup + private void afterStartup() { + started = true + } + + @PreDestroy + private void stop() { + destroyed = true + } + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/InheritedMethodsTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/InheritedMethodsTest.groovy new file mode 100644 index 000000000..55c915677 --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/InheritedMethodsTest.groovy @@ -0,0 +1,63 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.annotation.PostConstruct +import jakarta.inject.Inject +import jakarta.inject.Singleton + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +@TestGuiceyApp(App) +class InheritedMethodsTest extends Specification { + + @Inject + SampleBean bean + + def "Check inherited methods also called"() { + + expect: + bean.baseCalled + bean.called + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class BaseBean { + boolean baseCalled + + @PostConstruct + void initBase() { + baseCalled = true + } + } + + @Singleton + static class SampleBean extends BaseBean { + boolean called + + @PostConstruct + void init() { + called = true + } + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LazyBeansTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LazyBeansTest.groovy new file mode 100644 index 000000000..ee7bbec0c --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LazyBeansTest.groovy @@ -0,0 +1,69 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import spock.lang.Specification + +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy +import jakarta.inject.Singleton + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class LazyBeansTest extends Specification { + + + def "Check lazy initialization"() { + + when: "bean created with JIT and so being initialized AFTER events firing" + SampleBean bean = TestSupport.runWebApp(App, null) { + it.getInstance(SampleBean) + } + + then: + bean.initialized + bean.started + bean.destroyed + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Singleton + static class SampleBean { + boolean initialized + boolean started + boolean destroyed + + @PostConstruct + private void start() { + initialized = true + } + + @PostStartup + private void afterStartup() { + started = true + } + + @PreDestroy + private void stop() { + destroyed = true + } + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LifecycleTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LifecycleTest.groovy new file mode 100644 index 000000000..e2eef2e93 --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/LifecycleTest.groovy @@ -0,0 +1,85 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton +import ru.vyarus.dropwizard.guice.test.TestSupport +import spock.lang.Specification + +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class LifecycleTest extends Specification { + + def "Check full lifecycle events"() { + + when: + SampleBean bean = TestSupport.runWebApp(App, null) { + it.getInstance(SampleBean) + } + + then: + bean.initialized + bean.started + bean.destroyed + } + + + def "Check lightweight lifecycle events"() { + + when: + SampleBean bean = TestSupport.runCoreApp(App, null) { + it.getInstance(SampleBean) + } + + then: + bean.initialized + !bean.started // no server startup + bean.destroyed + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder() + // be sure bean initialized with the context and method would be processed when they appear + .extensions(SampleBean) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @EagerSingleton + static class SampleBean { + boolean initialized + boolean started + boolean destroyed + + @PostConstruct + private void start() { + initialized = true + } + + @PostStartup + private void afterStartup() { + started = true + } + + @PreDestroy + private void stop() { + destroyed = true + } + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/MethodFailuresTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/MethodFailuresTest.groovy new file mode 100644 index 000000000..703b2b52a --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/MethodFailuresTest.groovy @@ -0,0 +1,139 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton +import ru.vyarus.dropwizard.guice.test.TestSupport +import spock.lang.Specification + +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class MethodFailuresTest extends Specification { + + def "Check PostConstruct failure"() { + + when: 'start method throws exception' + TestSupport.runCoreApp(FailedApp) + + then: 'entire startup fails' + def ex = thrown(IllegalStateException) + ex.message.startsWith('Failed to execute method StartFailure.start of instance ru.vyarus.guicey.annotations.lifecycle.MethodFailuresTest$StartFailure') + } + + def "Check PostStartup failure"() { + + when: 'start server method throws exception' + TestSupport.runWebApp(ServerFailedApp) + + then: 'entire startup fails' + def ex = thrown(IllegalStateException) + ex.message.startsWith('Failed to execute method StartupFailure.afterStartup of instance ru.vyarus.guicey.annotations.lifecycle.MethodFailuresTest$StartupFailure') + } + + def "Check PreDestroy failure"() { + + when: 'destroy method throws exception' + DestroyFailure bean = TestSupport.runCoreApp(DestroyFailedApp, null) { + it.getInstance(DestroyFailure) + } + + then: 'exception suspended' + bean.called + } + + + def "Check lazy PostConstruct failure"() { + + when: 'start method throws exception, but called after event processing' + StartFailure bean = TestSupport.runCoreApp(App, null) { + it.getInstance(StartFailure) + } + + then: 'method called, error suppressed' + bean.called + } + + static class FailedApp extends App { + FailedApp() { + super(StartFailure) + } + } + + + static class ServerFailedApp extends App { + ServerFailedApp() { + super(StartupFailure) + } + } + + static class DestroyFailedApp extends App { + DestroyFailedApp() { + super(DestroyFailure) + } + } + + static class App extends Application { + + Class[] exts + + App() { + this(new Class[0]) + } + + App(Class... exts) { + this.exts = exts + } + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder() + // be sure bean initialized with the context and method would be processed when they appear + .extensions(exts) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @EagerSingleton + static class StartFailure { + boolean called + + @PostConstruct + private void start() { + called = true + throw new IllegalStateException() + } + } + + @EagerSingleton + static class StartupFailure { + + @PostStartup + private void afterStartup() { + throw new IllegalStateException() + } + } + + @EagerSingleton + static class DestroyFailure { + boolean called + + @PreDestroy + private void stop() { + called = true + throw new IllegalStateException() + } + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/PackageScopeTest.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/PackageScopeTest.groovy new file mode 100644 index 000000000..fa2d072bb --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/PackageScopeTest.groovy @@ -0,0 +1,49 @@ +package ru.vyarus.guicey.annotations.lifecycle + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guicey.annotations.lifecycle.support.SampleBean +import ru.vyarus.guicey.annotations.lifecycle.support.sub.AnotherBean +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +@TestGuiceyApp(App) +class PackageScopeTest extends Specification { + + @Inject + SampleBean bean + @Inject + AnotherBean anotherBean + + def "Check package scope"() { + + expect: "only bean in target package called" + !bean.called + anotherBean.called + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap + .addBundle(GuiceBundle.builder() + .bundles(new LifecycleAnnotationsBundle(AnotherBean.class.package.name)) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/SampleBean.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/SampleBean.groovy new file mode 100644 index 000000000..037ad5537 --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/SampleBean.groovy @@ -0,0 +1,19 @@ +package ru.vyarus.guicey.annotations.lifecycle.support + +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +@Singleton +class SampleBean { + + boolean called + + @PostConstruct + private void init() { + called = true + } +} diff --git a/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/sub/AnotherBean.groovy b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/sub/AnotherBean.groovy new file mode 100644 index 000000000..87ad1e1f8 --- /dev/null +++ b/guicey-lifecycle-annotations/src/test/groovy/ru/vyarus/guicey/annotations/lifecycle/support/sub/AnotherBean.groovy @@ -0,0 +1,16 @@ +package ru.vyarus.guicey.annotations.lifecycle.support.sub + +import jakarta.annotation.PostConstruct + +/** + * @author Vyacheslav Rusakov + * @since 27.11.2018 + */ +class AnotherBean { + boolean called + + @PostConstruct + private void init() { + called = true + } +} diff --git a/guicey-server-pages/README.md b/guicey-server-pages/README.md new file mode 100644 index 000000000..376f05a17 --- /dev/null +++ b/guicey-server-pages/README.md @@ -0,0 +1,867 @@ +# Guicey Server Pages + + +### About + +Brings the simplicity of JSP to dropwizard-views. +Basement for pluggable and extendable ui applications (like dashboards). + +**EXPERIMENTAL** + +Features: + +* Use standard dropwizard modules: [dropwizard-views](https://www.dropwizard.io/en/release-4.0.x/manual/views.html) and [dropwizard-assets](https://www.dropwizard.io/en/release-4.0.x/manual/core.html#serving-assets) +* Support direct templates rendering (without rest resource declaration) +* Static resources, direct templates and dropwizard-views rest endpoints are handled under the same url +(like everything is stored in the same directory - easy to link css, js and other resources) +* Multiple ui applications declaration with individual errors handling (error pages declaration like in servlet api, but not global) +* Ability to extend applications (much like good old resources copying above exploded war in tomcat) + +#### Problem + +Suppose you want to serve your ui to from the root url, then you need to re-map rest: + +```yaml +server: + rootPath: '/rest/*' + applicationContextPath: / +``` + +Static resources are in classpath: + +``` +com/something/ + index.html + style.css +``` + +Using dropwizard assets bundle to configure application: + +```java +bootstrap.addBundle(new AssetsBundle("/com/something/", "/", "index.html")); +``` + +Note that `index.html` could reference css with relative path: + +```html + +``` + +Now if we want to use template instead of pure html we configure dropwizard views: + +```java +bootstrap.addBundle(new ViewBundle()); +``` + +Renaming `index.html` to `index.ftl` and add view resource: + +```java +@Path("/ui/") +@Produces(MediaType.TEXT_HTML) +public class IndexResource { + + public static class IndexView extends View { + public IndexView() { + super("/com/something/index.ftl"); + } + } + + @GET + public IndexView get() { + return new IndexView(); + } +} +``` + +As a result, index page url become `/rest/ui/` so we need to link css resource with full path (`/style.css`) instead of relative +(or even re-configure server to back rest mapping to into root). + +It is already obvious that asset servlet and templates are not play well together. + +#### Solution + +The solution is obvious: make assets servlet as major resources supplier and with an additional filter to +detect template requests and redirect rendering to actual rest. + +So example above should become: + +``` +com/something/ + index.ftl + style.css +``` + +Where `index.ftl` could use + +```html + +``` + +because it is queried by url `/index.ftl`: no difference with usual `index.html` - template rendering is hidden +(and direct template file even don't need custom resource). + +When we need custom resource (most likely, for parameters mapping) we can still use it: + +```java +@Path("/views/ui/") +@Template("foo.ftl") +@Produces(MediaType.TEXT_HTML) +public class IndexResource { + + @GET + @Path("/foo/{id}") + public IndexView get(@PathParam("id") String id) { + return new TemplateView(); + } +} +``` + +It would be accessible from assets root `/foo/12` (more on naming and mapping details below). +Under the hood `/foo/12` will be recognized as template call and redirected (server redirect) to `/rest/ui/foo/12`. + +As you can see rest endpoints and templates are now "a part" of static resources.. just like good-old +JSP (powered with rest mappings). And it is still pure dropwizard views. + +GSP implements per-application error pages support so each application could use it's own errors. In pure +dropwizard-views such things should be implemented manually, which is not good for application incapsulation. + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-server-pages + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-server-pages:{guicey.version}' +``` + +Omit version if guicey BOM used. + +### Usage + +First of all, global GSP bundle must be installed in main application class. It +configures and installs dropwizard-views (global). It supports the same configurations as +pure dropwizard-views bundle. + +```java +GuiceBundle.builder() + .bundles(ServerPagesBundle.builder().build()); +``` + +IMPORTANT: Remove direct dropwizard-views bundle registrations (`ViewBundle`) if it was already used in application. + +#### Template engines + +Out of the box [dropwizard provides](https://www.dropwizard.io/en/release-4.0.x/manual/views.html) `freemarker` and `mustache` engines support. +You will need to add dependency to one of them (or both) in order to activate it (or, maybe, some third party engine): + +* implementation (`io.dropwizard:dropwizard-views-freemarker`) +* implementation (`io.dropwizard:dropwizard-views-mustache`) + +Other template engines available as 3rd party modules. If your template engine is not yet supported +then simply implement `io.dropwizard.views.common.ViewRenderer` in order to support it. + +`ViewRenderer` implementations are loaded automatically using ServiceLoader mechanism. +If your renderer is not declared as service then simply add it directly: + +```java +.bundles(ServerPagesBundle.builder() + .addViewRenderers(new MyTempateSupport()) + .build()); +``` + +Duplicate renderers are automatically removed. + +List of detected template engines will be printed to console. You can get list of used renderers +from bundle instance `ServerPagesBundle#getRenderers()` + +NOTE: this is pure dropwizard-views staff (everything is totally standard). + +#### Configuration + +Views yaml configuration binding is the same as in dropwizard-views. + +```yaml +views: + freemarker: + strict_syntax: true + mustache: + cache: false +``` + +Where `freemarker` and `mustache` are keys from installed template renderer +`io.dropwizard.views.common.ViewRenderer#getConfigurationKey()`. + +```java +public class AppConfig extends Configuration { + @JsonProperty + private Map> views; + + public Map> getViews() { return views;} +} +``` + +```java +.bundles(ServerPagesBundle.builder() + .viewsConfiguration(AppConfig::getViews) + .build()); +``` + +If `AppConfig#getViews` return `null` then empty map will be used instead as config. + +Additionally, to direct yaml configuration binding, you can apply exact template engine modifications + +```java +.bundles(ServerPagesBundle.builder() + .viewsConfiguration(AppConfig::getViews) + .viewsConfigurationModifier("freemarker", + map -> map.put("cache_storage", "freemarker.cache.NullCacheStorage")) + .build()); +``` + +Modifier always receive not null map (empty map is created automatically in global configuration). + +Multiple modifiers could be applied (even for the same section). Each GSP application could also apply modifiers +(this is useful to tune defaults: e.g. in case of freemarker, application may need to apply default imports). + +The final configuration (after all modifiers) could be printed to console with `.printViewsConfiguration()`. +Also, configuration is accessible from the bundle instance: `ServerPagesBundle#getViewsConfig()`. + +### Applications + +Each GSP application is registered as separate bundle in main or admin context: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .indexPage("index.ftl") + .build()) + +.bundles(ServerPagesBundle.adminApp("projectName-admin", "com.app.admin", "/admin") + .build()) +``` + +(Same inside `GuiceyBundle`) + +Unlimited number of applications may be registered on each context. + + +```java +app("projectName-ui", "com.app.ui", "/") +``` + +* `projectName-ui` - unique(!) application name. Uniqueness is very important as name used for rest paths. + To avoid collisions it's recommended to use domain-prefixed names to better identify application related resources. +* `com.app.ui` - classpath package with resources (application "root" folder; the same meaning as in dropwizard-assets); + Also, it may be configured as `/com/app/ui/`, but package notion is easier to understand +* `/` - application mapping url (in main or admin context; the same as in dropwizard-assets) + (if context is prefixed (`server.applicationContextPath: /some` or `server.adminContextPath: /admin`) then GSP + application will be available under this prefix) + +WARNING: It is a common desire to map ui on main context's root path (`/`), but, by default, dropwizard +maps rest there and so you may see an error: + +``` +java.lang.IllegalStateException: Multiple servlets map to path /*: app[mapped:JAVAX_API:null],io.dropwizard.jersey.setup.JerseyServletContainer-1280682[mapped:EMBEDDED:null] +``` + +In this case simply re-map rest in yaml config: +```yaml +server: + rootPath: '/rest/*' +``` + +If application requires resources from multiple paths, use: + +```java +ServerPagesBundle.app("projectName-ui", "com.app.path1", "/") + .attachAssets("com.app.path1") + ... +``` + +For example, this can be useful to attach some shared resources. +To attach webjars there is a [pre-defined shortcut](#webjars-usage). + +You can even attach resources path for exact sub url: + +```java +ServerPagesBundle.app("projectName-ui", "com.app.path1", "/") + .attachAssets("/sub/path/", "com.app.path.sub") + ... +``` + +And for urls starting from `/sub/path/` application will look static resources +(and templates) inside `/com/app/path/sub/` first, and only after that under root paths. + +This way, you can map resources from different packages as you want. This is like +if you copied everything from different packages into one place (like exploded war). + +#### Template engine constraint + +As GSP application declaration is separated from views configuration (GSP application +may be even a 3rd party bundle) then it must be able to check required template engines presence. + +For example, this application requires freemarker: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .requireRenderers("freemarker") + .build()) +``` + +Template engine name is declared in `io.dropwizard.views.common.ViewRenderer#getConfigurationKey()` (same name used in configuration). + +#### Templates support + +As dropwizard-views is used under the hood, all templates are always rendered with +rest endpoints. All these rest endpoints are part of global rest. + +It is recommended to start all view rest with `/view/` to make it clearly distinguishable +from application rest. Also, rest views, related to one GSP application must also start +with a common prefix: for example, `/view/projectName/ui/..`. + +You need to map required rest prefix in GSP application: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .mapViews("/view/projectName/ui/") +``` + +This will "map" all view rest paths after prefix directly to GSP application root. +So if you have view resource `/view/projectName/ui/page1/action` you can access it +relatively to application mapping root ("/" in the example above) as `/page1/action`. + +By default, if views mapping is not declared manually, it would be set to application name +(`/...` -> `/projectName-ui/...`) + +Under startup dropwizard logs all registered rest enpoints, so you can always see original +rest mapping paths. For each registered GSP application list of "visible" paths will be logged as: + +``` +INFO [2019-06-07 04:10:47,978] io.dropwizard.jersey.DropwizardResourceConfig: The following paths were found for the configured resources: + + GET /rest/views/projectName/ui/sample (com.project.ui.SampleViewResource) + POST /rest/views/projectName/ui/other (com.project.ui.SampleViewResource) + +INFO [2019-06-07 04:10:47,982] ru.vyarus.guicey.gsp.app.ServerPagesApp: Server pages app 'com.project.ui' registered on uri '/*' in main context + + Static resources locations: + com.app.ui + + Mapped handlers: + GET /sample (com.project.ui.SampleViewResource #sample) + POST /other (com.project.ui.SampleViewResource #other) +``` + +Here you can see real rest mapping `GET /rest/views/projectName/ui/sample` and +how it could be used relative to application path `GET /sample`. + +This report will always contain all correct view paths which must simplify overall understanding: +if path not appear in the report - it's incorrectly mapped and when it's appear - always use the path +from application report to access it. + +But that's not all: you can actually map other rest prefixed to sub urls: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .mapViews("/sub/path/", "/view/projectName2/ui/something/") +``` + +This way, it is possible to combine rest endpoints, written for different applications +(or simply prerare common view resource groups). Just note that in contrast to resources +mapping, only one prefix may be mapped on each url! + +You will also need to map static resources location accordingly if you use relative template paths. + +#### Direct templates + +You can also render template files without declaring view rest at all (good old jsp way). + +If we call supported template type directly like `http://localhost:8080/template.ftl` it will be recognized +as direct template call and rendered. Template file must be placed under registered classpath path root: +`/com/app/ui/template.ftl`. + +Templates in sub folders will be rendered the same way, e.g. `http://localhost:8080/sub/path/template.ftl` +will render `/com/app/ui/sub/path/template.ftl`. + +#### Template rest declaration + +Declaration differences with pure dropwizard-views: + +* `@Path` value must start with mapped prefix (see the chapter above) +* Resource class must be annotated with `@Template` (even without exact template declaration) +* `TemplateView` must be used instead of dropwizard `View` as a base class for view models. + +Suppose we declaring page for gsp application `.app("projectName-ui", "com.app.ui", "/")` + +As in pure views, in most cases we will need custom model object: + +```java +public class SampleView extends TemplateView { + private String name; + + public SampleView(String name) { + this.name = name; + } + + public String getName() { return this.name; } +} +``` + +NOTE: custom model is optional - you can use `TemplateView` directly, as default "empty" model. + +```java +@Path("/views/projectName/ui/sample/") +@Template("sample.ftl") +public class SamplePage { + + @Path("{name}") + public SampleView doSomething(@PathParam("name") String name) { + return new SampleView(name); + } +} +``` + +And example template: + +```ftl +<#-- @ftlvariable name="" type="com.project.ui.SampleView" --> + + + + + Sample page + + + + Name: ${name} + + +``` + +After application startup, new url must appear in GSP application console report. + +If we call new page with `http://localhost:8080/sample/fred` we should see +`Name: fred` as a result. + +NOTE: Can pure dropwizard-views resources be used like that? Actually, yes, but +they must be annotated with `@Template` because not annotated resources are not +considered as potential GSP application views (and will not be shown in console report). + +##### @Template + +`@Template` annotation must be used on ALL template resources. It may declare default +template for all methods in resource (`@Template("sample.ftl")`) or be just a marker annotation (`@Template`). + +Annotation differentiate template resources from other api resources and lets you delare jersey +extension only for template resources: + +```java +@Provider +@Template +public class MyExtensions implements ContainerRequestFilter { + ... +} +``` + +This request filter will be applied only to template resources. Such targeting is used +internally in order to not affect global api with GSP specific handling logic. + +Template path resolution: + +* If path starts with `/` then it would be resolved from classpath root +* Resolution relative to resource class +* Resolution relative to static resources location (`/com/app/ui/` in the example above) + +Examples: + +* `@Template("/com/project/custom/path/sample.ftl")` - absolute declaration. +* `@Template("sub/sample.ftl")` - relative declaration +* `@Template("../sub/sample.ftl")` - relative declaration + +Even if template is configured in the annotation, exact resource method could specify it's own +template directly in `TemplateView` constructor: + +```java +@Path("/views/projectName/ui/sample/") +@Template("sample.ftl") // default template +public class SamplePage { + + @Path("/") + public TemplateView doSomething() { + // override template + return new TemplateView("otherTemplate.ftl"); + } +} +``` + +Template path resolution rules are the same as with annotation. + +##### TemplateContext + +`TemplateContext` contains all template contextual information. It could be accessed inside template +with model's `getContext()`, e.g.: + +```ftl +<#-- @ftlvariable name="" type="com.project.ui.SampleView" --> + + + + + Sample page + + + + Current url: ${context.url} + + +``` + +In rest view resources it could be accessed with a static lookup: `TemplateContext.getInstance()`. + +This way you can always know current gsp application name, original url (before redirection to rest), +root application mapping prefix and get original request object (which may be required for error pages). + +#### Index page + +Index page is a page shown for root application url (`/`). It could be declared as: + +```java +.bundles(ServerPagesBundle.app("com.project.ui", "/com/app/ui/", "/") + .indexPage('index.html') + .build()) +``` + +It could be: +* Direct resource: `index.html` +* Direct template: `index.ftl` +* Rest powered template: `/mapping/` + +NOTE: By default, index page is set to `""` because most likely your index page will be +handled with rest and `""` will redirect to root path (for current application): `/com.project.ui/` + +#### Error pages + +Each GSP application could declare its own error pages (very similar to servlet api). + +It could be one global error page and different pages per status: + +```java +.bundles(ServerPagesBundle.app("com.project.ui", "/com/app/ui/", "/") + .errorPage('error.html') + .errorPage(500, '500.html') + .errorPage(401, '401.html') + .build()) +``` + +As with index pages, error page may be direct static html, direct template or rest path. + +IMPORTANT: error pages are shown ONLY if requested type was `text/html` and pass error as is +in other cases. Simply because it is not correct to return html when client was expecting different type. + +Errors handling logic detects: + +1. Static resources errors (404) +2. Exceptions during view resource processing (including rendering errors) +3. Direct error codes return without exception (`Resounce.status(404).build()`) + +Error pages may use special view class (or extend it) `ErrorTemplateView` which +collects additional error-related info. + +For example, even direct error template could show: + +```ftl +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.ErrorTemplateView" --> + +

        Url ${erroredUrl} failed to render with ${errorCode} status code

        +
        Error: ${error.class.simpleName}
        +
        +  ${errorTrace}
        +
        +``` + +For rest-powered error page: + +```java +@Path("/views/projectName/ui/error/") +@Template("error.ftl") +public class ErrorPage { + + @Path("/") + public TemplateView render() { + // it may be any extending class if additional properties are required (the same as usual) + ErrorTemplateView view = new ErrorTemplateView(); + WebApplicationException ex = view.getError(); + // analyze error + return view; + } +} +``` + +(this error page can be mapped as `.errorPage("/error/")`). + +`view.getError()` always return `WebApplicationException` so use `ex.geCause()` to get original exception. +But there will not always be useful exception because direct exception is only one of error cases (see above). + +In order to differentiate useful exceptions, you can check: + +```java +if (ex instanceof TracelessException) { + // only status code availbale + int status = ((TracelessException) ex).getStatus(); +} else { + // actually throwed exception to analyze + Throwable actualCause = ex.getCause() +} +``` + +`TracelessException` may be either `AssertError` for static resource fail or `TemplateRestCodeError` +for direct non 200 response code in rest. + +IMPORTANT: GSP errors handling override [ExceptionMapper](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#template-errors) +and [views errors](https://www.dropwizard.io/en/release-4.0.x/manual/views.html#custom-error-pages) +mechanisms because it intercept exceptions before them (using `RequestEventListener`)! So your +`ExceptionMapper` will be called, but user will still see GSP error page. + +The motivation is simple: otherwise it would be very hard to write side effect free GSP applications +because template resources exceptions could be intercepted with `ExceptionMapper`'s declared +in dropwizard application. + +To overcome this limitation, you can disable errors handling with `@ManuaErrorHandling`. +It may be applied on resource method or to resource class (to disable on all methods). + +For example: + +```java +@Path("/com.project.ui/error/") +@Template("page.ftl") +public class ErrorPage { + + @ManualErrorHandling + @Path("/") + public TemplateView render() { + // if exception appear inside this method, it would be handled with ExceptionMapper + // GSP error page will not be used + + // Also, if method return non 200 error code (>=400) like + // return Response.status(500).build() + // it would be also not handled with GSP error mechanism (only pure dropwizard staff) + } +} +``` + +Note that disabled errors will be indicated as `[DISABLED ERRORS]` in console report. + +#### SPA routing + +If you use Single Page Applications then you may face the need to recognize html5 client routing urls +and redirect to index page. You can read more about it in [guicey SPA module](../guicey-spa). + +As guicey SPA module can't be used directly with GSP, it's abilities is integrated directly and could +be activated with: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .spaRouting() + .build()) +``` + +Or, if custom detection regex is required: `.spaRouting(customRegex)` + +Most likely, usage use-case would be: index page requires some server-size templating. + +#### Template requests detection + +GSP must differentiate static resource calls from template calls. It assumes that static +resources would always end with an extension (e.g. `/something/some.ext`) and so: + +1. If request without extension - it's a template +2. If extension is recognized as template extension - render as template +3. Other cases are static resources + +The following regular expression used for extension detection: +```regexp +(?:^|/)([^/]+\.(?:[a-zA-Z\d]+))(?:\?.+)?$ +``` + +If it does not cover you specific cases, it could be changed using: + +```java +.bundles(ServerPagesBundle.app("com.project.ui", "/com/app/ui/", "/") + .filePattern("(?:^|/)([^/]+\\.(?:[a-zA-Z\\d]+))(?:\\?.+)?$") + .build()) +``` + +In case when you have static files without extension, you can include them directly +into detection regexp (using regex or (|) syntax). + +Pattern must return detected file name as first matched group (so direct template could be detected). +Pattern is searched (find) inside path, not matched (so simple patterns will also work). + +#### Extending applications + +In "war world" there is a a very handy thing as overlays: when we can apply our resources +"above" existing war. This way we can replace existing files (hack & tune) and add our own files +so they would live inside app as they were always be there. + +In order to achieve similar goals there is a application extension mechanism. + +For example we application: + +```java +.bundles(ServerPagesBundle.app("projectName-ui", "com.app.ui", "/") + .build()) +``` + +With multiple pages inside: + +``` +/com/app/ui/ + page1.ftl + page2.ftl + style.css +``` + +Each page could include style relatively as `style.css`. Most likely, there will even +be master template (freemarker) which unifies styles and common script installation. + +This application is distributed as 3rd party bundle (jar). If we need to add one more page +to this application in our current dropwizard application, we can: + +```java +.bundles(ServerPagesBundle.extendApp("projectName-ui") + .attachAssets("com.otherApp.ui.ext") + .build()) +``` + +And put another page into classpath: + +``` +/com/otherApp/ui/ext/ + page3.ftl +``` + +This page could also reference `style.css` relatively, the same as pages in the main application. + +On application startup, you will notice new resources location: + +``` + Static resources locations: + /com/app/ui/ + /com/otherApp/ui/ext/ +``` + +Now both locations are "roots" for the application. The same way as if we copied +`/com/otherApp/ui/ext/` into `/com/app/ui/`. + +`http://localhost:8080/page3.ftl` would correctly render new page. + +There may be unlimited number of application extensions. If extended application +is not available, it is not considered as an error: it's assumed as optional +application extension, which will be activated if some 3rd party jar with GSP application +appear in classpath. + +You can also map addition rest prefixes: + +```java +.bundles(ServerPagesBundle.extendApp("projectName-ui") + .mapViews("/sub/folder/", "/views/something/ext/") + .build()) +``` + +In some cases, extensions may depend on dropwizard configuration, but +bundles created under initialization phase. To workaround this you can +use delayed extensions init: + +```java +.bundles(ServerPagesBundle.extendApp("projectName-ui") + .delayedConfiguration((env, assets, views) -> { + if (env.configuration().isExtensionsEnabled()) { + assets.attach("com.foo.bar") + } + }) + .build()) +``` + +#### Webjars usage + +If you want to use resources from [webjars](https://www.webjars.org/) in GSP application: + +```java +.bundles(ServerPagesBundle.app("com.project.ui", "/com/app/ui/", "/") + .attachWebjars() + .build()) +``` + +For example, to add jquery: + +```groovy +implementation 'org.webjars.npm:jquery:3.4.1' +``` + +And it could be referenced as: + +```html + + + \ No newline at end of file diff --git a/guicey-server-pages/src/test/external/extapp/some.css b/guicey-server-pages/src/test/external/extapp/some.css new file mode 100644 index 000000000..6154e2e77 --- /dev/null +++ b/guicey-server-pages/src/test/external/extapp/some.css @@ -0,0 +1 @@ +/* external css */ \ No newline at end of file diff --git a/guicey-server-pages/src/test/external/extapp/template.ftl b/guicey-server-pages/src/test/external/extapp/template.ftl new file mode 100644 index 000000000..c8a9bda38 --- /dev/null +++ b/guicey-server-pages/src/test/external/extapp/template.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +page: ${context.url} \ No newline at end of file diff --git a/guicey-server-pages/src/test/external/extra/ext.ftl b/guicey-server-pages/src/test/external/extra/ext.ftl new file mode 100644 index 000000000..63be755d9 --- /dev/null +++ b/guicey-server-pages/src/test/external/extra/ext.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +external template \ No newline at end of file diff --git a/guicey-server-pages/src/test/external/extra/other.css b/guicey-server-pages/src/test/external/extra/other.css new file mode 100644 index 000000000..ee75f0bcc --- /dev/null +++ b/guicey-server-pages/src/test/external/extra/other.css @@ -0,0 +1 @@ +/* other css */ \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/AbstractTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/AbstractTest.groovy new file mode 100644 index 000000000..86db94ebd --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/AbstractTest.groovy @@ -0,0 +1,63 @@ +package ru.vyarus.guicey.gsp + +import org.apache.commons.text.StringEscapeUtils +import ru.vyarus.dropwizard.guice.test.ClientSupport +import spock.lang.Specification + +import jakarta.ws.rs.client.WebTarget +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2019 + */ +abstract class AbstractTest extends Specification { + + // default builder for text/html type (user call simulation) + ClientSupport client + + void setup(ClientSupport client) { + this.client = client + } + +// shortcut to return body + + String get(String url) { + call(main(), url, false) + } + + String getHtml(String url) { + call(main(), url, true) + } + + String adminGet(String url) { + call(admin(), url, false) + } + + String adminGetHtml(String url) { + call(admin(), url, true) + } + + private WebTarget main() { + return client.target('http://localhost:8080') + } + + private WebTarget admin() { + return client.target('http://localhost:8081') + } + + private String call(WebTarget http, String path, boolean html) { + Response res = http.path(path).request(html ? MediaType.TEXT_HTML : MediaType.TEXT_PLAIN).get() + if (res.status == 404) { + throw new FileNotFoundException() + } else if (res.status != 200) { + throw new IOException("status: ${res.status}") + } + String body = res.readEntity(String) + if (html) { + body = StringEscapeUtils.unescapeHtml4(body) + } + return body + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/DuplicateAppNameDetectionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/DuplicateAppNameDetectionTest.groovy new file mode 100644 index 000000000..0ff9f7c37 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/DuplicateAppNameDetectionTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2019 + */ +class DuplicateAppNameDetectionTest extends Specification { + + def "Check app collision detection"() { + + when: "starting app" + TestSupport.runWebApp(AppInit) + then: "duplicate name error" + def ex = thrown(IllegalArgumentException) + ex.message == 'Server pages application with name \'app\' is already registered' + } + + static class AppInit extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .indexPage("index.html") + .build(), + ServerPagesBundle.app("app", "/app", "/app") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/FilePatternChangeTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/FilePatternChangeTest.groovy new file mode 100644 index 000000000..8fa04b791 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/FilePatternChangeTest.groovy @@ -0,0 +1,53 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 30.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class FilePatternChangeTest extends AbstractTest { + + def "Check changed file detection regex"() { + + when: "accessing css resource" + String res = get("/css/style.css") + then: "ok" + res.contains("sample page css") + + when: "accessing template page" + res = getHtml("/template.ftl") + then: "template rendered" + res == "page: /template.ftl" + + when: "accessing html page" + getHtml("/index.html") + then: "path not found (detected that its not a template)" + thrown(FileNotFoundException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + // everything is a file, except direct .html files call + .filePattern("(?:^|/)([^/]+\\.(?:(?!html)|(?:css)|(?:ftl)))(?:\\?.+)?\$") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/IndexTemplateTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/IndexTemplateTest.groovy new file mode 100644 index 000000000..937202862 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/IndexTemplateTest.groovy @@ -0,0 +1,43 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 23.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class IndexTemplateTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing root" + String res = getHtml("/") + then: "index page" + res.contains("page: /") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("template.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MappingTest.groovy new file mode 100644 index 000000000..222c9361b --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MappingTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 14.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class MappingTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = getHtml("/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = get("/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MultipleAppsMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MultipleAppsMappingTest.groovy new file mode 100644 index 000000000..706a310c4 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/MultipleAppsMappingTest.groovy @@ -0,0 +1,98 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class MultipleAppsMappingTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/app") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = getHtml("/app/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = get("/app/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/app/template.ftl") + then: "rendered template" + res.contains("page: /app/template.ftl") + + when: "accessing template through resource" + res = getHtml("/app/sample/tt") + then: "template mapped" + res.contains("name: tt") + } + + def "Check app2 mapped"() { + + when: "accessing app" + String res = getHtml("/app2") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = getHtml("/app2/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = get("/app2/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/app2/template.ftl") + then: "rendered template" + res.contains("page: /app2/template.ftl") + + when: "accessing template through resource" + res = getHtml("/app2/sample/tt") + then: "template mapped" + res.contains("name: tt") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .mapViews("app") + .indexPage("index.html") + .build(), + ServerPagesBundle.app("app2", "/app", "/app2") + // use same rest as app + .mapViews("app") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplate.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplate.groovy new file mode 100644 index 000000000..6d5ac7228 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplate.groovy @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 24.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*", + configOverride = "server.applicationContextPath: /prefix/") +class NonRootIndexTemplate extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing root" + String res = getHtml("/prefix/") + then: "index page" + res.contains("page: /") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("template.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplateTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplateTest.groovy new file mode 100644 index 000000000..6e94ef94e --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootIndexTemplateTest.groovy @@ -0,0 +1,48 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 24.03.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class NonRootIndexTemplateTest extends AbstractTest { + + + def "Check non root mapping support"() { + + when: "accessing root with trailing slash" + String res = getHtml("/sub/") + then: "index page" + res.contains("page: /sub/") + + when: "accessing root without trailing slash" + res = getHtml("/sub") + then: "index page" + res.contains("page: /sub") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/sub") + .indexPage("template.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootMainContextTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootMainContextTest.groovy new file mode 100644 index 000000000..18de6faa5 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/NonRootMainContextTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*", + configOverride = "server.applicationContextPath: /prefix/") +class NonRootMainContextTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/prefix/") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = getHtml("/prefix/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = get("/prefix/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/prefix/template.ftl") + then: "rendered template" + res.contains("page: /prefix/template.ftl") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ResourceMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ResourceMappingTest.groovy new file mode 100644 index 000000000..7d2d2843e --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ResourceMappingTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 14.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ResourceMappingTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check custom resource mapping"() { + + when: "accessing template through resource" + String res = getHtml("/sample/tt") + then: "template mapped" + res.contains("name: tt") + + and: "recognized mappings" + info.getApplication("app").getViewPaths().collect { it.mappedUrl } as Set == [ + "/sample/error", + "/sample/error2", + "/sample/notfound", + "/sample/{name}"] as Set + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RestPathAsIndexPageTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RestPathAsIndexPageTest.groovy new file mode 100644 index 000000000..3fb2206ef --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RestPathAsIndexPageTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.views.template.Template + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class RestPathAsIndexPageTest extends AbstractTest { + + def "Check index page as path"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("root page from rest") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(RootPage) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("/root/") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Template + @Path("app/root") + static class RootPage { + + @GET + String get() { + return "root page from rest" + } + + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RootRestMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RootRestMappingTest.groovy new file mode 100644 index 000000000..f10e6d6ac --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/RootRestMappingTest.groovy @@ -0,0 +1,53 @@ +package ru.vyarus.guicey.gsp + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 24.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/*", + configOverride = "server.applicationContextPath: /prefix/") +class RootRestMappingTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/prefix/app/") + then: "index page" + res.contains("page: /") + + when: "accessing direct file" + res = getHtml("/prefix/app/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing direct template" + res = getHtml("/prefix/app/template.ftl") + then: "rendered template" + res.contains("page: /prefix/app/template.ftl") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .indexPage("template.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminMappingTest.groovy new file mode 100644 index 000000000..280ea3a26 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminMappingTest.groovy @@ -0,0 +1,59 @@ +package ru.vyarus.guicey.gsp.admin + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class AdminMappingTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = adminGetHtml("/appp/") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = adminGetHtml("/appp/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = adminGet("/appp/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = adminGetHtml("/appp/template.ftl") + then: "rendered template" + res.contains("page: /appp/template.ftl") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.adminApp("app", "/app", "/appp/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminResourceMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminResourceMappingTest.groovy new file mode 100644 index 000000000..ba0dfcd33 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/AdminResourceMappingTest.groovy @@ -0,0 +1,47 @@ +package ru.vyarus.guicey.gsp.admin + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class AdminResourceMappingTest extends AbstractTest { + + def "Chek custom resource mapping"() { + + when: "accessing template through resource" + String res = adminGetHtml("/appp/sample/tt") + then: "template mapped" + res.contains("name: tt") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.adminApp("app", "/app", "/appp/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/ComplexFlatMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/ComplexFlatMappingTest.groovy new file mode 100644 index 000000000..f01a9d035 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/admin/ComplexFlatMappingTest.groovy @@ -0,0 +1,62 @@ +package ru.vyarus.guicey.gsp.admin + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*", + configOverride = [ + "server.applicationContextPath: /prefix", + "server.adminContextPath: /admin"]) +class ComplexFlatMappingTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = adminGetHtml("/admin/ap/") + then: "index page" + res.contains("Sample page") + + when: "accessing direct file" + res = adminGetHtml("/admin/ap/index.html") + then: "index page" + res.contains("Sample page") + + when: "accessing resource" + res = adminGet("/admin/ap/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = adminGetHtml("/admin/ap/template.ftl") + then: "rendered template" + res.contains("page: /admin/ap/template.ftl") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.adminApp("app", "/app", "/ap") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/cases/ViewRestDetectionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/cases/ViewRestDetectionTest.groovy new file mode 100644 index 000000000..0a5c542e9 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/cases/ViewRestDetectionTest.groovy @@ -0,0 +1,62 @@ +package ru.vyarus.guicey.gsp.cases + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.views.template.Template + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 12.10.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ViewRestDetectionTest extends AbstractTest { + + def "Check view assigned"() { + + when: "accessing view" + String res = get("sample") + then: "view loaded" + res == 'something' + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .mapViews('/views/prefix/') + .build()) + .extensions(SampleRest) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + + // no leading slash! + @Path('views/prefix') + @Template + static class SampleRest { + + @GET + @Path("sample") + String get() { + return 'something' + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ApplicationInCustomClasspathTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ApplicationInCustomClasspathTest.groovy new file mode 100644 index 000000000..ab38c5b07 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ApplicationInCustomClasspathTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 09.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ApplicationInCustomClasspathTest extends AbstractTest { + + def "Check app resources access"() { + + when: "accessing html page" + String res = getHtml("/app/") + then: "resource found" + res.contains("Sample ext page") + + when: "accessing template" + res = getHtml("/app/template.ftl") + then: "resource found" + res.contains("page: /app/template.ftl") + + when: "accessing direct resource (through servlet)" + res = get("/app/some.css") + then: "resource found" + res.contains("external css ") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + ServerPagesBundle.app("app", "extapp", "/app", + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/CustomClasspathInExtensionCallbackTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/CustomClasspathInExtensionCallbackTest.groovy new file mode 100644 index 000000000..3b5bb822c --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/CustomClasspathInExtensionCallbackTest.groovy @@ -0,0 +1,67 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.app.asset.AssetSources +import ru.vyarus.guicey.gsp.app.ext.DelayedConfigurationCallback +import ru.vyarus.guicey.gsp.app.rest.mapping.ViewRestSources + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class CustomClasspathInExtensionCallbackTest extends AbstractTest { + + def "Check app resources access"() { + + when: "accessing external asset" + String res = getHtml("/app/ext.ftl") + then: "resource found" + res.contains("external template") + + when: "accessing direct resource (through servlet)" + res = getHtml("/app/other.css") + then: "resource found" + res.contains("other css") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + // app resources from classpath + ServerPagesBundle.app("app", "app", "/app") + .indexPage("index.html") + .build(), + // external assets + ServerPagesBundle.extendApp('app') + .attachAssets("extra") + .delayedConfiguration(new DelayedConfigurationCallback() { + @Override + void configure(GuiceyEnvironment environment, AssetSources assets, ViewRestSources views) { + assets.attach("extra", new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + } + }) + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ErrorPageFromCustomLoaderTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ErrorPageFromCustomLoaderTest.groovy new file mode 100644 index 000000000..c5e84f2d3 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ErrorPageFromCustomLoaderTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 14.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ErrorPageFromCustomLoaderTest extends AbstractTest { + + def "Check error page"() { + + when: "accessing not existing asset" + def res = getHtml("/notexisting.html") + then: "error page" + res.contains("Error: AssetError") + + when: "accessing not existing template" + res = getHtml("/notexisting.ftl") + then: "error page" + res.contains("Error: NotFoundException") + + when: "accessing not existing path" + res = getHtml("/notexisting/") + then: "error page" + res.contains("Error: NotFoundException") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + ServerPagesBundle.app("app", "extapp", "/", + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .errorPage("error.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ExtensionFromCustomClasspathTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ExtensionFromCustomClasspathTest.groovy new file mode 100644 index 000000000..cda941436 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/ExtensionFromCustomClasspathTest.groovy @@ -0,0 +1,63 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 09.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ExtensionFromCustomClasspathTest extends AbstractTest { + + def "Check app resources access"() { + + when: "accessing html page" + String res = getHtml("/app/") + then: "resource found" + res.contains("Sample page") + + when: "accessing external asset" + res = getHtml("/app/ext.ftl") + then: "resource found" + res.contains("external template") + + when: "accessing direct resource (through servlet)" + res = getHtml("/app/other.css") + then: "resource found" + res.contains("other css") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + // app resources from classpath + ServerPagesBundle.app("app", "app", "/app") + .indexPage("index.html") + .build(), + // external assets + ServerPagesBundle.extendApp('app', + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .attachAssets("extra") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideAssetsFromCustomLoaderTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideAssetsFromCustomLoaderTest.groovy new file mode 100644 index 000000000..cd33cb402 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideAssetsFromCustomLoaderTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class OverrideAssetsFromCustomLoaderTest extends AbstractTest { + + def "Check app resources access"() { + + when: "accessing html page" + String res = getHtml("/app/") + then: "page overridden" + res.contains("Sample ext page") + + when: "accessing direct resource (through servlet)" + res = get("/app/css/style.css") + then: "resource overridden" + res.contains("sample ext page css") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + // app resources from classpath + ServerPagesBundle.app("app", "app", "/app") + .indexPage("index.html") + .build(), + // overrides from custom classpath + ServerPagesBundle.extendApp('app', + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .attachAssets("extapp") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideCustomLoaderAssetsTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideCustomLoaderAssetsTest.groovy new file mode 100644 index 000000000..7d7ae1d22 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/OverrideCustomLoaderAssetsTest.groovy @@ -0,0 +1,59 @@ +package ru.vyarus.guicey.gsp.classloader + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class OverrideCustomLoaderAssetsTest extends AbstractTest { + // override custom class loader assets with assets from app class loader + + def "Check app resources access"() { + + when: "accessing html page" + String res = getHtml("/app/") + then: "page overridden" + res.contains("Sample page") + + when: "accessing direct resource (through servlet)" + res = get("/app/css/style.css") + then: "resource overridden" + res.contains("sample page css") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + .enableFreemarkerCustomClassLoadersSupport() + .build(), + // app resources from custom loader + ServerPagesBundle.app("app", "extapp", "/app", + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .indexPage("index.html") + .build(), + // overrides from application classpath + ServerPagesBundle.extendApp('app') + .attachAssets("app") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/TestTemplateFailWithoutActivatedSupportTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/TestTemplateFailWithoutActivatedSupportTest.groovy new file mode 100644 index 000000000..b8223e79a --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/classloader/TestTemplateFailWithoutActivatedSupportTest.groovy @@ -0,0 +1,60 @@ +package ru.vyarus.guicey.gsp.classloader + + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import java.nio.file.Paths + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2020 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class TestTemplateFailWithoutActivatedSupportTest extends AbstractTest { + + def "Check app resources access"() { + + when: "accessing html page" + String res = getHtml("/app/") + then: "resource found" + res.contains("Sample ext page") + + when: "accessing direct resource (through servlet)" + res = get("/app/some.css") + then: "resource found" + res.contains("external css ") + + when: "accessing template" + getHtml("/app/template.ftl") + then: "template rendering fails" + def ex = thrown(IOException) + ex.message == "status: 500" + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder() + //.enableFreemarkerCustomClassLoadersSupport() + .build(), + ServerPagesBundle.app("app", "extapp", "/app", + new URLClassLoader([Paths.get("src/test/external").toUri().toURL()] as URL[])) + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/AdminErrorMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/AdminErrorMappingTest.groovy new file mode 100644 index 000000000..1d6725e97 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/AdminErrorMappingTest.groovy @@ -0,0 +1,72 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class AdminErrorMappingTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + def res = adminGetHtml("/appp/notexisting.html") + then: "error page" + res.contains("custom error page") + + when: "accessing not existing template" + res = adminGetHtml("/appp/notexisting.ftl") + then: "error page" + res.contains("custom error page") + + when: "accessing not existing path" + res = adminGetHtml("/appp/notexisting/") + then: "error page" + res.contains("custom error page") + + when: "error processing template" + res = adminGetHtml("/appp/sample/error") + then: "error page" + res.contains("custom error page") + + when: "error processing template" + res = adminGetHtml("/appp/sample/error2") + then: "error page" + res.contains("custom error page") + + when: "direct 404 rest response" + res = adminGetHtml("/appp/sample/notfound") + then: "error page" + res.contains("custom error page") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.adminApp("app", "/app", "/appp") + .indexPage("index.html") + .errorPage("error.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorCodesMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorCodesMappingTest.groovy new file mode 100644 index 000000000..749737427 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorCodesMappingTest.groovy @@ -0,0 +1,83 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.views.template.Template + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 24.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class ErrorCodesMappingTest extends AbstractTest { + + def "Check error mapping"() { + + when: "error processing template" + def res = getHtml("/code/403") + then: "error page" + res.contains("Error code: 403") + + when: "error processing template" + res = getHtml("/code/405") + then: "error page" + res.contains("Error code2: 405") + + when: "accessing not existing asset" + getHtml("/notexisting.html") + then: "no error mapped" + thrown(FileNotFoundException) + + when: "accessing not existing template" + getHtml("/notexisting.ftl") + then: "no error mapped" + thrown(FileNotFoundException) + + when: "error processing template" + getHtml("/code/407") + then: "no error mapped" + thrown(IOException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(ErrorResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("err", "/err", "/") + .errorPage(403, "error.ftl") + .errorPage(405, "error2.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Path('/err/') + @Template + public static class ErrorResource { + + @GET + @Path("/code/{code}") + public Response get(@PathParam("code") Integer code) { + return Response.status(code).build() + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorMappingTest.groovy new file mode 100644 index 000000000..dc511967e --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorMappingTest.groovy @@ -0,0 +1,72 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 15.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ErrorMappingTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + def res = getHtml("/notexisting.html") + then: "error page" + res.contains("custom error page") + + when: "accessing not existing template" + res = getHtml("/notexisting.ftl") + then: "error page" + res.contains("custom error page") + + when: "accessing not existing path" + res = getHtml("/notexisting/") + then: "error page" + res.contains("custom error page") + + when: "error processing template" + res = getHtml("/sample/error") + then: "error page" + res.contains("custom error page") + + when: "error processing template" + res = getHtml("/sample/error2") + then: "error page" + res.contains("custom error page") + + when: "direct 404 rest response" + res = getHtml("/sample/notfound") + then: "error page" + res.contains("custom error page") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .errorPage("error.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderErrorPageTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderErrorPageTest.groovy new file mode 100644 index 000000000..5b6e8ccc7 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderErrorPageTest.groovy @@ -0,0 +1,71 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 30.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ErrorRenderErrorPageTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + getHtml("/notexisting.html") + then: "error page failed to render" + thrown(FileNotFoundException) + + when: "accessing not existing template" + getHtml("/notexisting.ftl") + then: "error page failed to render" + thrown(FileNotFoundException) + + when: "accessing not existing path" + getHtml("/notexisting/") + then: "error page failed to render" + thrown(FileNotFoundException) + + when: "error processing template" + getHtml("/sample/error") + then: "error page failed to render (500)" + thrown(IOException) + + when: "error processing template" + getHtml("/sample/error2") + then: "error page failed to render (500)" + thrown(IOException) + + when: "direct 404 rest response" + getHtml("/sample/notfound") + then: "error page failed to render" + thrown(FileNotFoundException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("/sample/error") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderingTemplateTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderingTemplateTest.groovy new file mode 100644 index 000000000..64ece4bef --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorRenderingTemplateTest.groovy @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 15.12.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class ErrorRenderingTemplateTest extends AbstractTest { + + def "Check error mapping"() { + + when: "failed template render" + def res = getHtml("/failed.ftl") + then: "error page failed to render" + res == "Error: WebApplicationException" + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("error.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorTemplateTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorTemplateTest.groovy new file mode 100644 index 000000000..5d54d05c9 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ErrorTemplateTest.groovy @@ -0,0 +1,71 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 23.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class ErrorTemplateTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + def res = getHtml("/notexisting.html") + then: "error page" + res.contains("Error: AssetError") + + when: "accessing not existing template" + res = getHtml("/notexisting.ftl") + then: "error page" + res.contains("Error: NotFoundException") + + when: "accessing not existing path" + res = getHtml("/notexisting/") + then: "error page" + res.contains("Error: NotFoundException") + + when: "error processing template" + res = getHtml("/sample/error") + then: "error page" + res.contains("Error: WebApplicationException") + + when: "error processing template" + res = getHtml("/sample/error2") + then: "error page" + res.contains("Error: WebApplicationException") + + when: "direct 404 rest response" + res = getHtml("/sample/notfound") + then: "error page" + res.contains("Error: TemplateRestCodeError") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("error.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ExceptionMapperInterceptionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ExceptionMapperInterceptionTest.groovy new file mode 100644 index 000000000..b5fe91f93 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ExceptionMapperInterceptionTest.groovy @@ -0,0 +1,86 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.views.template.ManualErrorHandling +import ru.vyarus.guicey.gsp.views.template.Template + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.core.Response +import jakarta.ws.rs.ext.ExceptionMapper +import jakarta.ws.rs.ext.Provider + +/** + * @author Vyacheslav Rusakov + * @since 09.06.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ExceptionMapperInterceptionTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing throwing resource" + def res = getHtml("/err") + then: "gsp error page" + res == "Error: WebApplicationException" + + when: "accessing throwing resource with disabled error mechanism" + res = getHtml("/err2") + then: "manual error handling" + res == "handled!" + } + + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(ErrRest, ExHandler) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("test.app", "/app", "/") + .errorPage("error.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + + @Path("/test.app/") + @Template + public static class ErrRest { + + @Path("/err") + @GET + public String get() { + throw new IllegalArgumentException("Sample error") + } + + @ManualErrorHandling + @Path("/err2") + @GET + public String get2() { + throw new IllegalArgumentException("Sample error") + } + } + + @Provider + public static class ExHandler implements ExceptionMapper { + @Override + Response toResponse(IllegalArgumentException exception) { + return Response.ok("handled!").build() + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/MimeTypeRecognitionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/MimeTypeRecognitionTest.groovy new file mode 100644 index 000000000..10b7629a1 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/MimeTypeRecognitionTest.groovy @@ -0,0 +1,112 @@ +package ru.vyarus.guicey.gsp.error + + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource +import spock.lang.Specification + +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class MimeTypeRecognitionTest extends Specification { + + def "Check error mapping"(ClientSupport client) { + + when: "accessing not existing asset" + def res = client.targetApp('/notexisting.html').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "accessing not existing asset with text result" + res = client.targetApp('/notexisting.html').request(MediaType.TEXT_PLAIN).get() + then: "no error page" + res.status == 404 + + + when: "accessing not existing template" + res = client.targetApp('/notexisting.ftl').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "accessing not existing template with text result" + res = client.targetApp('/notexisting.ftl').request(MediaType.TEXT_PLAIN).get() + then: "no error page" + res.status == 404 + + + when: "accessing not existing path" + res = client.targetApp('/notexisting/').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "accessing not existing path with text result" + res = client.targetApp('/notexisting/').request(MediaType.TEXT_PLAIN).get() + then: "no error page" + res.status == 404 + + + when: "error processing template" + res = client.targetApp('/sample/error').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "error processing template with text result" + res = client.targetApp('/sample/error').request(MediaType.TEXT_PLAIN).get() + then: "no error page" + res.status == 500 + + + when: "error processing template" + res = client.targetApp('/sample/error2').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "error processing template with text result" + res = client.targetApp('/sample/error2').request(MediaType.TEXT_PLAIN).get() + then: "no error page" + res.status == 500 + + + when: "direct 404 rest response" + res = client.targetApp('/sample/notfound').request(MediaType.TEXT_HTML).get() + then: "error page" + res.readEntity(String) == "custom error page" + + when: "direct 404 rest response with text result" + res = client.targetApp('/sample/notfound').request(MediaType.TEXT_PLAIN).get() + then: "error page" + res.status == 404 + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .errorPage("error.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NoRootMappingTemplateErrorsTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NoRootMappingTemplateErrorsTest.groovy new file mode 100644 index 000000000..7748e5ec6 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NoRootMappingTemplateErrorsTest.groovy @@ -0,0 +1,72 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 23.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml', + configOverride = "server.applicationContextPath: /prefix/") +class NoRootMappingTemplateErrorsTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + def res = getHtml("/prefix/notexisting.html") + then: "error page" + res.contains("Error: AssetError") + + when: "accessing not existing template" + res = getHtml("/prefix/notexisting.ftl") + then: "error page" + res.contains("Error: NotFoundException") + + when: "accessing not existing path" + res = getHtml("/prefix/notexisting/") + then: "error page" + res.contains("Error: NotFoundException") + + when: "error processing template" + res = getHtml("/prefix/sample/error") + then: "error page" + res.contains("Error: WebApplicationException") + + when: "error processing template" + res = getHtml("/prefix/sample/error2") + then: "error page" + res.contains("Error: WebApplicationException") + + when: "direct 404 rest response" + res = getHtml("/prefix/sample/notfound") + then: "error page" + res.contains("Error: TemplateRestCodeError") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("error.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NonErrorInterceptionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NonErrorInterceptionTest.groovy new file mode 100644 index 000000000..f0a8e0188 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/NonErrorInterceptionTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.guicey.gsp.error + + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class NonErrorInterceptionTest extends Specification { + + def "Check non error forwarding"(ClientSupport client) { + + when: "calling for non 200 response" + def res = client.targetApp('/res').request(MediaType.TEXT_HTML).get() + then: "redirect" + res.status == 304 + + when: "direct rest non 200 return" + res = client.targetApp('/res/2').request(MediaType.TEXT_HTML).get() + then: "redirect" + res.status == 304 + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(Resource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("error.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Path("/app/res") + static class Resource { + + @GET + @Path("/") + void get() { + throw new WebApplicationException(304) + } + + + @GET + @Path("/2") + jakarta.ws.rs.core.Response get2() { + return jakarta.ws.rs.core.Response.status(304).build() + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/RestPathAsErrorPageTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/RestPathAsErrorPageTest.groovy new file mode 100644 index 000000000..06f35abd4 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/RestPathAsErrorPageTest.groovy @@ -0,0 +1,60 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.views.template.Template + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class RestPathAsErrorPageTest extends AbstractTest { + + def "Check index page as path"() { + + when: "accessing not existing asset" + def res = getHtml("/notexisting.html") + then: "error page" + res.contains("error page from rest") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(ErrorPage) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("/error/") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + @Template + @Path("app/error") + static class ErrorPage { + + @GET + String get() { + return "error page from rest" + } + + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ShowTraceOnErrorPageTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ShowTraceOnErrorPageTest.groovy new file mode 100644 index 000000000..cd7ba19f0 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/error/ShowTraceOnErrorPageTest.groovy @@ -0,0 +1,71 @@ +package ru.vyarus.guicey.gsp.error + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.SampleTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 29.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ShowTraceOnErrorPageTest extends AbstractTest { + + def "Check error mapping"() { + + when: "accessing not existing asset" + def res = getHtml("/notexisting.html") + then: "error page" + res.startsWith("ru.vyarus.guicey.gsp.app.filter.AssetError: Error serving asset /notexisting.html: 404") + + when: "accessing not existing template" + res = getHtml("/notexisting.ftl") + then: "error page" + res.startsWith("jakarta.ws.rs.NotFoundException: Template 'notexisting.ftl' not found") + + when: "accessing not existing path" + res = getHtml("/notexisting/") + then: "error page" + res.startsWith("jakarta.ws.rs.NotFoundException: HTTP 404 Not Found") + + when: "error processing template" + res = getHtml("/sample/error") + then: "error page" + res.startsWith("jakarta.ws.rs.WebApplicationException: HTTP 500 Internal Server Error") + + when: "error processing template" + res = getHtml("/sample/error2") + then: "error page" + res.startsWith("jakarta.ws.rs.WebApplicationException: error") + + when: "direct 404 rest response" + res = getHtml("/sample/notfound") + then: "error page" + res.startsWith("ru.vyarus.guicey.gsp.app.rest.support.TemplateRestCodeError: Error processing template rest call app/sample/notfound: 404") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(SampleTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .errorPage("error2.ftl") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/DelayedAppExtensionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/DelayedAppExtensionTest.groovy new file mode 100644 index 000000000..05a77a03a --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/DelayedAppExtensionTest.groovy @@ -0,0 +1,101 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.support.app.OverridableTemplateResource +import ru.vyarus.guicey.gsp.support.app.SubTemplateResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class DelayedAppExtensionTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("overridden sample page") + + when: "accessing direct file" + res = getHtml("/index.html") + then: "index page" + res.contains("overridden sample page") + + when: "accessing resource" + res = get("/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + + when: "accessing direct ext template" + res = getHtml("/ext.ftl") + then: "rendered template" + res.contains("ext template") + + when: "accessing path" + res = getHtml("/sample") + then: "index page" + res.contains("page: /sample") + + when: "accessing sub mapped path" + res = getHtml("/sub/sample") + then: "index page" + res.contains("page: /sub/sample") + + and: "mapping correct" + info.getApplication("app").getViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/sample", + "/sample"] as Set + + info.getApplication("app").getHiddenViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/{name}"] as Set + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(OverridableTemplateResource, SubTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build(), + ServerPagesBundle.extendApp("app") + .delayedConfiguration({ env, assets, views -> + assert env + assert assets + assert views + assets.attach("/ext") + views.map("/sub", "/sub") + }) + .build()) + .build()) + + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedAppTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedAppTest.groovy new file mode 100644 index 000000000..9a57fe7bd --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedAppTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 21.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ExtendedAppTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("overridden sample page") + + when: "accessing direct file" + res = getHtml("/index.html") + then: "index page" + res.contains("overridden sample page") + + when: "accessing resource" + res = get("/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + + when: "accessing direct ext template" + res = getHtml("/ext.ftl") + then: "rendered template" + res.contains("ext template") + + when: "accessing ext template through mapping" + res = getHtml("/sample/ext.ftl") + then: "rendered template" + res.contains("ext template") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build(), + ServerPagesBundle.extendApp("app") + .attachAssets("/ext") + .attachAssets("/sample", "ext") + .build()) + .build()) + + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedFromGuiceyTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedFromGuiceyTest.groovy new file mode 100644 index 000000000..7575e12be --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtendedFromGuiceyTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class ExtendedFromGuiceyTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("overridden sample page") + + when: "accessing direct file" + res = getHtml("/index.html") + then: "index page" + res.contains("overridden sample page") + + when: "accessing resource" + res = get("/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + + when: "accessing direct ext template" + res = getHtml("/ext.ftl") + then: "rendered template" + res.contains("ext template") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build(), + new GuiceyBundle() { + @Override + void initialize(GuiceyBootstrap gb) { + gb.bundles(ServerPagesBundle.extendApp("app") + .attachAssets("/ext") + .build()) + } + }) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtensionForAppRegisteredInGuiceyTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtensionForAppRegisteredInGuiceyTest.groovy new file mode 100644 index 000000000..10a4878a2 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ExtensionForAppRegisteredInGuiceyTest.groovy @@ -0,0 +1,68 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class ExtensionForAppRegisteredInGuiceyTest extends AbstractTest { + + def "Check app mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("overridden sample page") + + when: "accessing direct file" + res = getHtml("/index.html") + then: "index page" + res.contains("overridden sample page") + + when: "accessing resource" + res = get("/css/style.css") + then: "css" + res.contains("/* sample page css */") + + when: "accessing direct template" + res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + + when: "accessing direct ext template" + res = getHtml("/ext.ftl") + then: "rendered template" + res.contains("ext template") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + // extension registered before application + ServerPagesBundle.extendApp("app") + .attachAssets("/ext") + .build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/MappedAssetsTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/MappedAssetsTest.groovy new file mode 100644 index 000000000..5f4248c36 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/MappedAssetsTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class MappedAssetsTest extends AbstractTest { + + def "Check assets mapped"() { + + when: "accessing mapped url" + String res = getHtml("/sample/ext.ftl") + then: "rendered template" + res.contains("ext template") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .attachAssets('/sample', 'ext') + .build()) + .build()) + + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ViewsExtensionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ViewsExtensionTest.groovy new file mode 100644 index 000000000..cb04cb79d --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/ViewsExtensionTest.groovy @@ -0,0 +1,70 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.support.app.OverridableTemplateResource +import ru.vyarus.guicey.gsp.support.app.SubTemplateResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 03.12.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ViewsExtensionTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check app mapped"() { + + when: "accessing path" + String res = getHtml("/sample") + then: "index page" + res.contains("page: /sample") + + when: "accessing sub mapped path" + res = getHtml("/sub/sample") + then: "index page" + res.contains("page: /sub/sample") + + and: "mapping correct" + info.getApplication("app").getViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/sample", + "/sample"] as Set + + info.getApplication("app").getHiddenViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/{name}"] as Set + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(OverridableTemplateResource, SubTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .build(), + ServerPagesBundle.extendApp("app") + .mapViews("/sub", "/sub") + .build()) + .build()) + + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/WebjarsIntegrationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/WebjarsIntegrationTest.groovy new file mode 100644 index 000000000..bcf75692f --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/ext/WebjarsIntegrationTest.groovy @@ -0,0 +1,45 @@ +package ru.vyarus.guicey.gsp.ext + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 11.06.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class WebjarsIntegrationTest extends Specification { + + def "Check webjars binding"() { + + when: "accessing jquery script" + def res = new URL("http://localhost:8080/jquery/3.4.1/dist/jquery.min.js").text + then: "ok" + res.contains("jQuery v3.4.1") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .attachWebjars() + .build()) + .build()) + + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ApplicationInfoTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ApplicationInfoTest.groovy new file mode 100644 index 000000000..a2996f4dd --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ApplicationInfoTest.groovy @@ -0,0 +1,146 @@ +package ru.vyarus.guicey.gsp.info + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.relative.RelativeTemplateResource +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 04.12.2019 + */ +@TestDropwizardApp(App) +class ApplicationInfoTest extends Specification { + + @Inject + GspInfoService info + + def "Check app info correctness"() { + + expect: + info.getApplications().size() == 2 + + and: "app - mostly default mappings, but with extensions" + with(info.getApplication("app")) { + name == "app" + mainContext + mappingUrl == "/app/" + rootUrl == "/app/" + requiredRenderers == ["freemarker"] + + mainAssetsLocation == "app/" + assetExtensions.size() == 1 + assetExtensions.get("") as List == ["foo/"] as List + viewExtensions.size() == 1 + viewExtensions.get("foo/") == "foo/" + with(assets) { + keySet().size() == 1 + it.get("") as List == ["foo/", "app/"] as List + } + with(views) { + keySet().size() == 2 + it[""] == "app/" + it["foo/"] == "foo/" + } + mainRestPrefix == "app/" + restRootUrl == "/" + + indexFile == "" + filesRegex != null + hasDefaultFilesRegex + + !spa + spaRegex != null + hasDefaultSpaRegex + + errorPages.size() == 1 + errorPages[404] == "err.tpl" + defaultErrorPage == null + + hiddenViewPaths.isEmpty() + viewPaths.size() == 3 + } + + and: "app2 - customized mappings" + with(info.getApplication("app2")) { + name == "app2" + !mainContext + mappingUrl == "/app2/" + rootUrl == "/app2/" + requiredRenderers.isEmpty() + + mainAssetsLocation == "app/" + assetExtensions.isEmpty() + viewExtensions.isEmpty() + with(assets) { + keySet().size() == 2 + it.get("") as List == ["foo/bar/", "app/"] as List + it.get("some/") as List == ["bazz/"] as List + } + with(views) { + keySet().size() == 2 + it[""] == "app/" + it["some/"] == "bazz/" + } + mainRestPrefix == "app/" + restRootUrl == "/" + + indexFile == "sample.html" + filesRegex == "someregex" + !hasDefaultFilesRegex + + spa + spaRegex == "otherregex" + !hasDefaultSpaRegex + + errorPages.size() == 1 + errorPages[-1] == "err.tpl" + defaultErrorPage == "err.tpl" + + hiddenViewPaths.isEmpty() + viewPaths.size() == 3 + } + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "app", "/app") + .errorPage(404, "err.tpl") + .requireRenderers("freemarker") + .build(), + ServerPagesBundle.adminApp("app2", "app", "/app2") + .indexPage("sample.html") + .attachAssets("foo.bar") + .attachAssets("/some", "bazz") + .mapViews("app") + .mapViews("/some", "/bazz") + .errorPage("err.tpl") + .filePattern("someregex") + .spaRouting("otherregex") + .build(), + ServerPagesBundle.extendApp("app") + .mapViews("/foo", "foo") + .attachAssets("foo") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(RelativeTemplateResource) + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ViewsInfoTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ViewsInfoTest.groovy new file mode 100644 index 000000000..749e178d0 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/info/ViewsInfoTest.groovy @@ -0,0 +1,79 @@ +package ru.vyarus.guicey.gsp.info + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.views.common.View +import io.dropwizard.views.common.ViewRenderer +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 04.12.2019 + */ +@TestDropwizardApp(value = App) +class ViewsInfoTest extends Specification { + + @Inject + GspInfoService info + + def "Check views info"() { + + expect: "information correct" + with(info.getViewsConfig()) { + size() == 1 + it["foo"].size() == 1 + it["foo"]["some"] == "bar" + } + info.getViewRendererNames() as Set == ["freemarker", "mustache", "foo"] as Set + info.getViewRenderers().size() == 3 + } + + static class App extends Application { + + ServerPagesBundle bundle + + @Override + void initialize(Bootstrap bootstrap) { + bundle = + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder() + .addViewRenderers(new ViewRenderer() { + @Override + boolean isRenderable(View view) { + return false + } + + @Override + void render(View view, Locale locale, OutputStream output) throws IOException { + + } + + @Override + void configure(Map options) { + + } + + @Override + String getConfigurationKey() { + return "foo" + } + }) + .viewsConfiguration({ ['foo': ['some': 'bar']] }) + .printViewsConfiguration() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/AdminSpaRoutingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/AdminSpaRoutingTest.groovy new file mode 100644 index 000000000..49b9ef701 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/AdminSpaRoutingTest.groovy @@ -0,0 +1,51 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class AdminSpaRoutingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = adminGetHtml("/app") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = adminGetHtml("/app/some") + then: "error" + res.contains("Sample page") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.adminApp("app", "/app", "/app") + .indexPage("index.html") + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomIndexTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomIndexTest.groovy new file mode 100644 index 000000000..ebf833461 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomIndexTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class CustomIndexTest extends AbstractTest { + + def "Check custom index"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("other index page") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle + .app("app", "/app", "/") + .indexPage("idx.htm") + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomRegexTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomRegexTest.groovy new file mode 100644 index 000000000..a8db1a926 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/CustomRegexTest.groovy @@ -0,0 +1,51 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class CustomRegexTest extends AbstractTest { + + def "Check custom regex"() { + + when: "accessing html" + String res = getHtml("/some/some.html") + then: "index page" + res.contains("Sample page") + + when: "accessing js" + get("/some/some.js") + then: "index page" + thrown(FileNotFoundException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle + .app("app", "/app", "/") + .indexPage("index.html") + .spaRouting("\\.js\$") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/FlatAdminMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/FlatAdminMappingTest.groovy new file mode 100644 index 000000000..0d0ba7de4 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/FlatAdminMappingTest.groovy @@ -0,0 +1,52 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/flat.yml') +class FlatAdminMappingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = getHtml("/admin/app") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = getHtml("/admin/app/some") + then: "index page" + res.contains("Sample page") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle + .adminApp("app", "/app", "/app") + .indexPage("index.html") + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/GuiceyIntegrationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/GuiceyIntegrationTest.groovy new file mode 100644 index 000000000..013aae337 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/GuiceyIntegrationTest.groovy @@ -0,0 +1,66 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class GuiceyIntegrationTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = getHtml("/some/") + then: "ok" + res.contains("Sample page") + + when: "accessing not existing resource" + getHtml("/some.html") + then: "error" + thrown(FileNotFoundException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(ServerPagesBundle.builder().build(), + new AppBundle()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class AppBundle implements GuiceyBundle { + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.bundles( + ServerPagesBundle + .app('app', '/app', '/') + .indexPage('index.html') + .spaRouting() + .build() + ) + } + } +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/MultipleBundlesMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/MultipleBundlesMappingTest.groovy new file mode 100644 index 000000000..f75f16c6b --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/MultipleBundlesMappingTest.groovy @@ -0,0 +1,120 @@ +package ru.vyarus.guicey.gsp.spa + +import com.google.common.net.HttpHeaders +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 18.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class MultipleBundlesMappingTest extends AbstractTest { + + def "Check spa mappings"() { + + when: "first" + String res = getHtml("/1") + then: "index page" + res.contains("Sample page") + + when: "second" + res = getHtml("/2") + then: "index page" + res.contains("Sample page") + + when: "admin first" + res = adminGetHtml("/a1") + then: "index page" + res.contains("Sample page") + + when: "admin second" + res = adminGetHtml("/a2") + then: "index page" + res.contains("Sample page") + + + when: "accessing not existing page" + res = getHtml("/2/some/") + then: "error" + res.contains("Sample page") + + when: "accessing not existing admin page" + res = adminGetHtml("/a2/some/") + then: "error" + res.contains("Sample page") + } + + def "Check cache header"(ClientSupport client) { + + when: "calling first path" + def res = client.targetApp('/1').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "calling second path" + res = client.targetApp('/2').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect" + res = client.targetApp('/1/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect 2" + res = client.targetApp('/2/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "direct index page" + res = client.targetApp('/1/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + + when: "direct index page 2" + res = client.targetApp('/2/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app1", "/app", "/1") + .indexPage('index.html') + .spaRouting() + .build(), + ServerPagesBundle.app("app2", "/app", "/2") + .indexPage('index.html') + .spaRouting() + .build(), + ServerPagesBundle.adminApp("aapp1", "/app", "/a1") + .indexPage('index.html') + .spaRouting() + .build(), + ServerPagesBundle.adminApp("aapp2", "/app", "/a2") + .indexPage('index.html') + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRedirectionErrorTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRedirectionErrorTest.groovy new file mode 100644 index 000000000..f94769826 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRedirectionErrorTest.groovy @@ -0,0 +1,52 @@ +package ru.vyarus.guicey.gsp.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 06.02.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/conf.yml') +class SpaRedirectionErrorTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing not existing page" + def res = getHtml("/some/") + then: "error page instead of index" + res.contains("custom error page") + + when: "accessing not existing resource" + res = getHtml("/some.html") + then: "error page instead of index" + res.contains("custom error page") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + // bad index page + .indexPage("/sample/error") + .errorPage("error.html") + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRoutingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRoutingTest.groovy new file mode 100644 index 000000000..f4d0db434 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/spa/SpaRoutingTest.groovy @@ -0,0 +1,117 @@ +package ru.vyarus.guicey.gsp.spa + +import com.google.common.net.HttpHeaders +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 14.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class SpaRoutingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = getHtml("/") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = getHtml("/some/") + then: "ok" + res.contains("Sample page") + + when: "accessing not existing resource" + getHtml("/some.html") + then: "error" + thrown(FileNotFoundException) + } + + def "Check no cache header"(ClientSupport client) { + + when: "calling index" + def res = client.targetApp('/').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect" + res = client.targetApp('/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "direct index page" + res = client.targetApp('/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + + when: "resource" + res = client.targetApp('/css/style.css').request().get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + } + + def "Chck different mime type"() { + + when: "calling with html type" + def res = client.targetApp('/some').request(MediaType.TEXT_HTML).get() + then: "redirect" + res.status == 200 + + when: "calling with text type" + res = client.targetApp('/some').request(MediaType.TEXT_PLAIN).get() + then: "no redirect" + res.status == 404 + + when: "calling with unknown content type" + res = client.targetApp('/some').request("abrakadabra").get() + then: "no redirect" + res.status == 404 + + when: "calling with empty type not allowed" + res = client.targetApp('/some').request(" ").get() + then: "empty response type " + res.status == 404 + + } + + def "Check non 404 error"() { + + when: "calling for cached content" + def res = client.targetApp('/index.html').request(MediaType.TEXT_HTML) + .header('If-Modified-Since', 'Wed, 21 Oct 2215 07:28:00 GMT').get() + then: "cached" + res.status == 304 + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .indexPage("index.html") + .spaRouting() + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/OverridableTemplateResource.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/OverridableTemplateResource.groovy new file mode 100644 index 000000000..b5074f695 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/OverridableTemplateResource.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.gsp.support.app + +import ru.vyarus.guicey.gsp.views.template.Template +import ru.vyarus.guicey.gsp.views.template.TemplateView + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2019 + */ +@Path("/app/") +@Template("/app/template.ftl") +class OverridableTemplateResource { + + @Path("/sub/{name}") + @GET + TemplateView getSub() { + return new TemplateView() + } + + @Path("/sample") + @GET + TemplateView getSample() { + return new TemplateView() + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SampleTemplateResource.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SampleTemplateResource.groovy new file mode 100644 index 000000000..4790c6e5a --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SampleTemplateResource.groovy @@ -0,0 +1,47 @@ +package ru.vyarus.guicey.gsp.support.app + +import ru.vyarus.guicey.gsp.views.template.Template +import ru.vyarus.guicey.gsp.views.template.TemplateView + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 14.01.2019 + */ +@Path("/app/sample/") +@Template("/app/sample.ftl") +class SampleTemplateResource { + + @Path("/{name}") + @GET + SampleModel get(@PathParam("name") String name) { + return new SampleModel(name: name); + } + + @Path("/error") + @GET + SampleModel error() { + throw new IllegalStateException("error"); + } + + @Path("/error2") + @GET + SampleModel error2() { + throw new WebApplicationException("error"); + } + + @Path("/notfound") + @GET + Response notfound() { + Response.status(404).build(); + } + + public static class SampleModel extends TemplateView { + String name + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SubTemplateResource.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SubTemplateResource.groovy new file mode 100644 index 000000000..c81290ead --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/SubTemplateResource.groovy @@ -0,0 +1,22 @@ +package ru.vyarus.guicey.gsp.support.app + +import ru.vyarus.guicey.gsp.views.template.Template +import ru.vyarus.guicey.gsp.views.template.TemplateView + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2019 + */ +@Path("/sub/") +@Template("/app/template.ftl") +class SubTemplateResource { + + @Path("/sample") + @GET + TemplateView getSub() { + return new TemplateView() + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/clash/BaseViewResource.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/clash/BaseViewResource.groovy new file mode 100644 index 000000000..289b9840d --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/app/clash/BaseViewResource.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.gsp.support.app.clash + +import ru.vyarus.guicey.gsp.views.template.Template +import ru.vyarus.guicey.gsp.views.template.TemplateView + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2019 + */ +@Path("/foo/") +@Template("/app/template.ftl") +class BaseViewResource { + + @Path("/one") + @GET + TemplateView getSub1() { + return new TemplateView() + } + + @Path("/bar/two") + @GET + TemplateView getSub2() { + return new TemplateView() + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/relative/RelativeTemplateResource.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/relative/RelativeTemplateResource.groovy new file mode 100644 index 000000000..a709b5daa --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/support/relative/RelativeTemplateResource.groovy @@ -0,0 +1,38 @@ +package ru.vyarus.guicey.gsp.support.relative + +import ru.vyarus.guicey.gsp.views.template.Template +import ru.vyarus.guicey.gsp.views.template.TemplateView + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2019 + */ +@Path("/app/relative/") +// template declaration relative to class +@Template("relative.ftl") +class RelativeTemplateResource { + + @Path("/direct") + @GET + TemplateView get() { + return new TemplateView() + } + + + @Path("/relative") + @GET + TemplateView getRelative() { + // relative template name also resolved relative to resource class + return new TemplateView("../root.ftl") + } + + @Path("/dir") + @GET + TemplateView getDir() { + // relative template to classpath resources dir + return new TemplateView("template.ftl") + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ClashingDefaultHandlersMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ClashingDefaultHandlersMappingTest.groovy new file mode 100644 index 000000000..d45208d07 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ClashingDefaultHandlersMappingTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.support.app.clash.BaseViewResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ClashingDefaultHandlersMappingTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check direct template in sub mapping"() { + + when: "accessing base mapping" + String res = getHtml("/app/one") + then: "index page" + res.contains("page: /app/one") + + when: "accessing base mapping, clashing with direct template from other sub mapping" + res = getHtml("/app/bar/two") + then: "index page" + res.contains("page: /app/bar/two") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(BaseViewResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "app", "/app") + .mapViews("/foo/") + .mapViews("/bar", "/foo/bar/") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/DifferentViewPrefixeDirectTemplateTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/DifferentViewPrefixeDirectTemplateTest.groovy new file mode 100644 index 000000000..dcbe96b26 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/DifferentViewPrefixeDirectTemplateTest.groovy @@ -0,0 +1,55 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.support.app.OverridableTemplateResource +import ru.vyarus.guicey.gsp.support.app.SubTemplateResource + +/** + * @author Vyacheslav Rusakov + * @since 03.12.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class DifferentViewPrefixeDirectTemplateTest extends AbstractTest { + + def "Check direct template in sub mapping"() { + + when: "accessing direct template" + String res = getHtml("/app/template.ftl") + then: "index page" + res.contains("page: /app/template.ftl") + + when: "accessing sub mapping direct template" + res = getHtml("/app/sub/subtemplate.ftl") + then: "index page" + res.contains("page: /app/sub/subtemplate.ftl subcontext: /sub/") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(OverridableTemplateResource, SubTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .mapViews("app") + .mapViews("/sub", "/sub") + .attachAssets("/sub", "/app/sub") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/NoViewsSupportTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/NoViewsSupportTest.groovy new file mode 100644 index 000000000..41f113650 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/NoViewsSupportTest.groovy @@ -0,0 +1,45 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 05.06.2019 + */ +class NoViewsSupportTest extends Specification { + + def "Check view support absence detection"() { + + when: "starting app" + TestSupport.runWebApp(App) + then: "no views support detected" + def ex = thrown(IllegalStateException) + ex.message == 'Either server pages support bundle was not installed (use ServerPagesBundle.builder() to create bundle) or it was installed after \'app\' application bundle' + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + + + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + // NO global setup + ServerPagesBundle.app("app", "/app", "/").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/PrintEmptyConfigurationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/PrintEmptyConfigurationTest.groovy new file mode 100644 index 000000000..cf3bece3f --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/PrintEmptyConfigurationTest.groovy @@ -0,0 +1,43 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 07.02.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class PrintEmptyConfigurationTest extends Specification { + + def "Check empty config printing"() { + + expect: "created main config empty map correctlu prointed" + true + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder() + .viewsConfiguration({ null }) + .printViewsConfiguration() + .build(), + ServerPagesBundle.app("app", "/app", "/").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RelativeTemplateResolutionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RelativeTemplateResolutionTest.groovy new file mode 100644 index 000000000..5017c7f6e --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RelativeTemplateResolutionTest.groovy @@ -0,0 +1,69 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.support.relative.RelativeTemplateResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 27.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class RelativeTemplateResolutionTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check relative templates"() { + + when: "template from annotation" + String res = getHtml("/relative/direct") + then: "found" + res.contains("name: app") + + when: "template relative to class" + res = getHtml("/relative/relative") + then: "found" + res.contains("root name: app") + + when: "template relative to dir" + res = getHtml("/relative/dir") + then: "found" + res.contains("page: /relative/dir") + + and: "mapping correct" + info.getApplication("app").getViewPaths().collect { it.mappedUrl } as Set == [ + "/relative/dir", + "/relative/direct", + "/relative/relative"] as Set + + info.getApplication("app").getHiddenViewPaths().isEmpty() + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(RelativeTemplateResource) + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RendererDetectionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RendererDetectionTest.groovy new file mode 100644 index 000000000..68f21a239 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RendererDetectionTest.groovy @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.TestSupport +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 05.06.2019 + */ +class RendererDetectionTest extends Specification { + + def "Check renderer requirement check"() { + + when: "starting app" + TestSupport.runWebApp(App) + then: "absent renderer detected" + def ex = thrown(IllegalStateException) + ex.message == 'Required template engines are missed for server pages application \'app\': fooo (available engines: freemarker, mustache)' + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .requireRenderers("fooo") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RenderersRegistrationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RenderersRegistrationTest.groovy new file mode 100644 index 000000000..01bc595c0 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/RenderersRegistrationTest.groovy @@ -0,0 +1,84 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import io.dropwizard.views.common.View +import io.dropwizard.views.common.ViewRenderer +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 26.01.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class RenderersRegistrationTest extends Specification { + + def "Check renderers registration"() { + + expect: "duplicate renderer removed" + true + } + + static class App extends Application { + + ServerPagesBundle bundle + + @Override + void initialize(Bootstrap bootstrap) { + bundle = ServerPagesBundle.builder() + .addViewRenderers( + new CustomRenderer("r1"), new CustomRenderer("r2"), + new CustomRenderer("r2"), new CustomRenderer("r3")) + .printViewsConfiguration() + .build() + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + bundle, + ServerPagesBundle.app("app", "/app", "/").build(), + ServerPagesBundle.app("app2", "/app", "/2").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + assert bundle.getRenderers().collect { + it.getConfigurationKey() + } as Set == ['freemarker', 'mustache', 'r1', 'r2', 'r3'] as Set + } + } + + static class CustomRenderer implements ViewRenderer { + + String key + + CustomRenderer(String key) { + this.key = key + } + + @Override + boolean isRenderable(View view) { + return false + } + + @Override + void render(View view, Locale locale, OutputStream output) throws IOException { + + } + + @Override + void configure(Map options) { + + } + + @Override + String getConfigurationKey() { + return key + } + } + +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/SuccessRendererDetectionTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/SuccessRendererDetectionTest.groovy new file mode 100644 index 000000000..7e8efc26b --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/SuccessRendererDetectionTest.groovy @@ -0,0 +1,42 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 05.06.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class SuccessRendererDetectionTest extends Specification { + + def "Check success requirement check"() { + + expect: "required template renderer present" + true + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/") + .requireRenderers('freemarker') + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewConfigurationModificationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewConfigurationModificationTest.groovy new file mode 100644 index 000000000..2a2e9469a --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewConfigurationModificationTest.groovy @@ -0,0 +1,58 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 05.06.2019 + */ +@TestDropwizardApp(value = App) +class ViewConfigurationModificationTest extends Specification { + + @Inject + GspInfoService info + + def "Check views configuration modification in app"() { + + expect: "application started without errors" + def config = info.getViewsConfig() + config['freemarker']['cache_storage'] == 'yes' + config['test'].isEmpty() + } + + static class App extends Application { + + ServerPagesBundle bundle + + @Override + void initialize(Bootstrap bootstrap) { + bundle = ServerPagesBundle.builder() + .printViewsConfiguration() + .build() + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + bundle, + ServerPagesBundle.app("app", "/app", "/app") + .viewsConfigurationModifier('freemarker', { it['cache_storage'] = "yes" }) + .viewsConfigurationModifier('test', {}) + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + assert bundle.getViewsConfig()['freemarker']['cache_storage'] == 'yes' + assert bundle.getViewsConfig()['test'].isEmpty() + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigCreationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigCreationTest.groovy new file mode 100644 index 000000000..b5cced739 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigCreationTest.groovy @@ -0,0 +1,48 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.ServerPagesBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 07.02.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ViewsConfigCreationTest extends Specification { + + def "Check null views configuration binding"() { + + expect: "created main config map and sub map for freemarker" + true + } + + static class App extends Application { + + ServerPagesBundle bundle + + @Override + void initialize(Bootstrap bootstrap) { + bundle = ServerPagesBundle.builder() + .viewsConfiguration({ null }) + .viewsConfigurationModifier('freemarker', { it['foo'] = 'bar' }) + .printViewsConfiguration() + .build() + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + bundle, + ServerPagesBundle.app("app", "/app", "/").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + assert bundle.getViewsConfig()['freemarker']['foo'] == 'bar' + } + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigurationTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigurationTest.groovy new file mode 100644 index 000000000..b17bf6017 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsConfigurationTest.groovy @@ -0,0 +1,62 @@ +package ru.vyarus.guicey.gsp.views + +import com.fasterxml.jackson.annotation.JsonProperty +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle + +/** + * @author Vyacheslav Rusakov + * @since 26.01.2019 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/views.yml') +class ViewsConfigurationTest extends AbstractTest { + + def "Check views configuration binding"() { + + when: "accessing direct template" + def res = getHtml("/template.ftl") + then: "rendered template" + res.contains("page: /template.ftl") + } + + static class App extends Application { + + ServerPagesBundle bundle + + @Override + void initialize(Bootstrap bootstrap) { + bundle = ServerPagesBundle.builder() + .viewsConfiguration({ it.views }) + // used to assert global config binding + .viewsConfigurationModifier('freemarker', { assert it['cache_storage'] != null }) + .printViewsConfiguration() + .build() + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + bundle, + ServerPagesBundle.app("app", "/app", "/").build()) + .build()) + } + + @Override + void run(Config configuration, Environment environment) throws Exception { + assert bundle.getRenderers().size() == 2 + assert bundle.getViewsConfig() != null + // value from yaml + assert bundle.getViewsConfig()['freemarker']['cache_storage'] == 'freemarker.cache.NullCacheStorage' + } + } + + static class Config extends Configuration { + + @JsonProperty + Map> views; + + } +} diff --git a/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsSubMappingTest.groovy b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsSubMappingTest.groovy new file mode 100644 index 000000000..48f3e7c65 --- /dev/null +++ b/guicey-server-pages/src/test/groovy/ru/vyarus/guicey/gsp/views/ViewsSubMappingTest.groovy @@ -0,0 +1,82 @@ +package ru.vyarus.guicey.gsp.views + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp +import ru.vyarus.guicey.gsp.AbstractTest +import ru.vyarus.guicey.gsp.ServerPagesBundle +import ru.vyarus.guicey.gsp.info.GspInfoService +import ru.vyarus.guicey.gsp.info.model.GspApp +import ru.vyarus.guicey.gsp.support.app.OverridableTemplateResource +import ru.vyarus.guicey.gsp.support.app.SubTemplateResource + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 02.12.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class ViewsSubMappingTest extends AbstractTest { + + @Inject + GspInfoService info + + def "Check app mapped"() { + + when: "accessing path" + String res = getHtml("/app/sample") + then: "index page" + res.contains("page: /app/sample") + + when: "accessing sub mapped path" + res = getHtml("/app/sub/sample") + then: "index page" + res.contains("page: /app/sub/sample") + + when: "get info" + GspApp app = info.getApplication("app") + then: "info correct" + with(app.getViews()) { + size() == 2 + it[""] == "app/" + it["sub/"] == "sub/" + } + with(app.getAssets()) { + size() == 1 + it.get("") == ["app/"] + } + app.indexFile == "index.html" + + info.getApplication("app").getViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/sample", + "/sample"] as Set + + info.getApplication("app").getHiddenViewPaths().collect { it.mappedUrl } as Set == [ + "/sub/{name}"] as Set + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .extensions(OverridableTemplateResource, SubTemplateResource) + .bundles( + ServerPagesBundle.builder().build(), + ServerPagesBundle.app("app", "/app", "/app") + .mapViews("app") + .mapViews("/sub/", "/sub/") + .indexPage("index.html") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-server-pages/src/test/resources/app/css/style.css b/guicey-server-pages/src/test/resources/app/css/style.css new file mode 100644 index 000000000..aeb366bd6 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/css/style.css @@ -0,0 +1,4 @@ +/* sample page css */ +body { + font-size: 12px; +} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/directTemplate.ftl b/guicey-server-pages/src/test/resources/app/directTemplate.ftl new file mode 100644 index 000000000..e69de29bb diff --git a/guicey-server-pages/src/test/resources/app/error.ftl b/guicey-server-pages/src/test/resources/app/error.ftl new file mode 100644 index 000000000..59b38ef17 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/error.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.ErrorTemplateView" --> +Error: ${error.class.simpleName} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/error.html b/guicey-server-pages/src/test/resources/app/error.html new file mode 100644 index 000000000..ca90cf588 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/error.html @@ -0,0 +1 @@ +custom error page \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/error2.ftl b/guicey-server-pages/src/test/resources/app/error2.ftl new file mode 100644 index 000000000..c8d93bae5 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/error2.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.ErrorTemplateView" --> +${errorTrace} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/failed.ftl b/guicey-server-pages/src/test/resources/app/failed.ftl new file mode 100644 index 000000000..23aa003af --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/failed.ftl @@ -0,0 +1 @@ +${unknownField} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/idx.htm b/guicey-server-pages/src/test/resources/app/idx.htm new file mode 100644 index 000000000..d4f6d5221 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/idx.htm @@ -0,0 +1 @@ +other index page \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/index.html b/guicey-server-pages/src/test/resources/app/index.html new file mode 100644 index 000000000..ee710479b --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/index.html @@ -0,0 +1,14 @@ + + + + + Sample page + + + +
        + Sample page +
        + + + \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/sample.ftl b/guicey-server-pages/src/test/resources/app/sample.ftl new file mode 100644 index 000000000..06047a245 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/sample.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.support.app.SampleTemplateResource.SampleModel" --> +name: ${name} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/sub/subtemplate.ftl b/guicey-server-pages/src/test/resources/app/sub/subtemplate.ftl new file mode 100644 index 000000000..59ce04335 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/sub/subtemplate.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +page: ${context.url} subcontext: ${context.restSubContext} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/app/template.ftl b/guicey-server-pages/src/test/resources/app/template.ftl new file mode 100644 index 000000000..c8a9bda38 --- /dev/null +++ b/guicey-server-pages/src/test/resources/app/template.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +page: ${context.url} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/conf.yml b/guicey-server-pages/src/test/resources/conf.yml new file mode 100644 index 000000000..169e33973 --- /dev/null +++ b/guicey-server-pages/src/test/resources/conf.yml @@ -0,0 +1,8 @@ +server: + rootPath: '/rest/*' + +logging: + level: INFO + + loggers: + ru.vyarus: DEBUG diff --git a/guicey-server-pages/src/test/resources/err/error.ftl b/guicey-server-pages/src/test/resources/err/error.ftl new file mode 100644 index 000000000..a1048393c --- /dev/null +++ b/guicey-server-pages/src/test/resources/err/error.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.ErrorTemplateView" --> +Error code: ${errorCode} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/err/error2.ftl b/guicey-server-pages/src/test/resources/err/error2.ftl new file mode 100644 index 000000000..6303be43a --- /dev/null +++ b/guicey-server-pages/src/test/resources/err/error2.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.ErrorTemplateView" --> +Error code2: ${errorCode} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/ext/error.html b/guicey-server-pages/src/test/resources/ext/error.html new file mode 100644 index 000000000..a5d7ddd0e --- /dev/null +++ b/guicey-server-pages/src/test/resources/ext/error.html @@ -0,0 +1 @@ +overridden error page \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/ext/ext.ftl b/guicey-server-pages/src/test/resources/ext/ext.ftl new file mode 100644 index 000000000..548c2b65a --- /dev/null +++ b/guicey-server-pages/src/test/resources/ext/ext.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +ext template \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/ext/index.html b/guicey-server-pages/src/test/resources/ext/index.html new file mode 100644 index 000000000..2d395ea76 --- /dev/null +++ b/guicey-server-pages/src/test/resources/ext/index.html @@ -0,0 +1 @@ +overridden sample page \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/flat.yml b/guicey-server-pages/src/test/resources/flat.yml new file mode 100644 index 000000000..af961e727 --- /dev/null +++ b/guicey-server-pages/src/test/resources/flat.yml @@ -0,0 +1,20 @@ +server: + rootPath: '/rest/*' + + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 + + requestLog: + appenders: + - type: console + timeZone: Asia/Novosibirsk + +logging: + level: INFO + + loggers: + ru.vyarus: DEBUG \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/relative/relative.ftl b/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/relative/relative.ftl new file mode 100644 index 000000000..508bcba13 --- /dev/null +++ b/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/relative/relative.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +name: ${context.appName} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/root.ftl b/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/root.ftl new file mode 100644 index 000000000..8251104ad --- /dev/null +++ b/guicey-server-pages/src/test/resources/ru/vyarus/guicey/gsp/support/root.ftl @@ -0,0 +1,2 @@ +<#-- @ftlvariable name="" type="ru.vyarus.guicey.gsp.views.template.TemplateView" --> +root name: ${context.appName} \ No newline at end of file diff --git a/guicey-server-pages/src/test/resources/views.yml b/guicey-server-pages/src/test/resources/views.yml new file mode 100644 index 000000000..ce21ec35a --- /dev/null +++ b/guicey-server-pages/src/test/resources/views.yml @@ -0,0 +1,13 @@ +server: + rootPath: '/rest/*' + +logging: + level: INFO + + loggers: + ru.vyarus: DEBUG + +views: + freemarker: + # no cache for local dev + cache_storage: freemarker.cache.NullCacheStorage \ No newline at end of file diff --git a/guicey-spa/README.md b/guicey-spa/README.md new file mode 100644 index 000000000..f7e5d977c --- /dev/null +++ b/guicey-spa/README.md @@ -0,0 +1,134 @@ +# Single page applications support + +### About + +Provides a replacement for dropwizard-assets bundle for single page applications (SPA) to properly +handle html5 client routing. + +Features: + +* Pure dropwizard bundle, but can be used with guicey bundles +* Build above dropwizard-assets servlet +* Support registration on main and admin contexts +* Multiple apps could be registered +* Sets no-cache headers for index page +* Regex could be used to tune routes detection + +#### Problem + +The problem with SPA is html5 routing. For example, suppose your app base url is `/app` +and client route url is `/app/someroute` (before there were no problem because route would +look like `/app/#!/someroute`). When user hit refresh (or bookmark) such route, server is actually +called with route url. Server must recognize it and return index page. + +For example, Angular 2 router use html5 mode my default. + +#### Solution + +The problem consists of two points: + +1. Correctly process resource calls (css, js, images, etc) and return 404 for missed resources +2. Recognize application routes and return index page instead + +Bundles register dropwizard-assets servlet with special filter above it. Filter tries to process +all incoming urls. This approach grants that all calls to resources will be processed and +index page will not be returned instead of resource (solves problem 1). + +If resource is not found - index page returned. To avoid redirection in case of bad resources request, +filter will redirect only requests accepting 'text/html'. Additional regexp (configurable) +is used to recognize most resource calls and avoid redirection (show correct 404). + +From example above, `/app/someroute` will return index page and `/app/css/some.css` will return css. +`/app/css/unknown.css` will return 404 as resource call will be recognized and css file is not exists. + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-spa + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-spa:{guicey.version}' +``` + +Omit version if guicey BOM used. + +### Usage + +Register bundle: + +```java +GuiceBundle.builder() + .bundles(SpaBundle.app("app", "/app", "/").build()); +``` + +This will register app with name "app" (name is used to name servlets and filters and must be unique). +Application files are located in "app" package in classpath (e.g. resources inside jar). +Application is mapped to root context (note that this will work only if rest is mapped +to some sub context: e.g. with `server.rootPath: '/rest/*'` configuration). + +``` +http://localhost:8080/ -> app index +http://loclahost:8080/css/app.css -> application resource, located at /app/css/app.css in classpath +http://localhost:8080/someroute -> application client route - index page returned +``` + +Example registration to admin context: + +```java +.bundles(SpaBundle.adminApp("admin", "com.mycompany.adminapp", "/manager").build()); +``` + +Register "admin" application with resources in "/com/mycompany/adminapp/" package, served from "manager' +admin context (note that admin root is already used by dropwizard admin servlet). + +NOTE: resources location can be declared both as path (`/com/mycompany/adminapp/`) or as package (`com.mycompany.adminapp`). + +``` +http://localhost:8081/manager -> admin app index +``` + +You can register as many apps as you like. They just must use different urls and have different names: + +```java +.bundles(SpaBundle.app("app", "/app", "/").build(), + SpaBundle.app("app2", "/app2", "/").build(), + SpaBundle.adminApp("admin", "/com/mycompany/adminapp/", "/manager").build(), + SpaBundle.adminApp("admin2", "/com/mycompany/adminapp2/", "/manager2").build()); +``` + +#### Index page + +By default, index page assumed to be "index.html". Could be changed with: + +```java +.bundles(SpaBundle.app("app", "/app", "/").indexPage("main.html").build()); +``` + +#### Prevent redirect regex + +By default, the following regex is used to prevent resources redirection (to not send index for missed resource): + +```regexp +\.(html|css|js|png|jpg|jpeg|gif|ico|xml|rss|txt|eot|svg|ttf|woff|woff2|cur)(\?((r|v|rel|rev)=[\-\.\w]*)?)?$ +``` + +Could be changed with: + +```java +.bundles(SpaBundle.app("app", "/app", "/") + .preventRedirectRegex("\\.\\w{2,5}(\\?.*)?$") + .build()); +``` + +This regexp implements naive assumption that all app routes does not contain "extension". + +Note: regexp is applied with `find` so use `^` or `$` to apply boundaries. diff --git a/guicey-spa/build.gradle b/guicey-spa/build.gradle new file mode 100644 index 000000000..4a25282ba --- /dev/null +++ b/guicey-spa/build.gradle @@ -0,0 +1,8 @@ +description = "Singe page applications support" + +dependencies { + implementation 'io.dropwizard:dropwizard-assets' + + testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8' + testImplementation 'org.objenesis:objenesis:3.4' +} \ No newline at end of file diff --git a/guicey-spa/src/main/java/ru/vyarus/guicey/spa/SpaBundle.java b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/SpaBundle.java new file mode 100644 index 000000000..d14f300df --- /dev/null +++ b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/SpaBundle.java @@ -0,0 +1,174 @@ +package ru.vyarus.guicey.spa; + +import com.google.common.base.Joiner; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.jetty.setup.ServletEnvironment; +import io.dropwizard.servlets.assets.AssetServlet; +import jakarta.servlet.DispatcherType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.guicey.spa.filter.SpaBundleState; +import ru.vyarus.guicey.spa.filter.SpaRoutingFilter; + +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static ru.vyarus.dropwizard.guice.module.installer.util.PathUtils.SLASH; + +/** + * Provides support for SPA (for example, angular apps). + * Such applications often use html5 pretty url, like "/app/someroute/subroute". + * When user bookmark such url or simply refresh page, browser requests complete url on server + * and server must support it: redirect to main index page (without changing url, so client could handle routing). + *

        + * Use dropwizard-assets servlet internally, but wraps it with special filter, which reacts on resource not found + * errors (by default, all calls pass to assets filter!). Applies no-cache header for index page. + *

        + * You can register multiple SPA applications on main or admin contexts (or both). + * All applications must have unique names. In case of duplicate names or mapping on the same path, error + * will be thrown. + * + * @author Vyacheslav Rusakov + * @since 02.04.2017 + */ +public class SpaBundle implements GuiceyBundle { + + /** + * Default asset pattern. + */ + public static final String DEFAULT_PATTERN = + "\\.(html|css|js|png|jpg|jpeg|gif|ico|xml|rss|txt|eot|svg|ttf|woff|woff2|cur)" + + "(\\?((r|v|rel|rev)=[\\-\\.\\w]*)?)?$"; + + private final Logger logger = LoggerFactory.getLogger(SpaBundle.class); + + private boolean mainContext; + private String assetName; + private String resourcePath; + private String uriPath; + private String indexFile = "index.html"; + private String noRedirectRegex = DEFAULT_PATTERN; + + @Override + public void initialize(final GuiceyBootstrap bootstrap) { + // state shared between all spa bundles + // NOTE: not a static field for proper parallel tests support + bootstrap.sharedState(SpaBundleState.class, SpaBundleState::new).checkUnique(assetName); + } + + @Override + @SuppressWarnings("PMD.LooseCoupling") + public void run(final GuiceyEnvironment environment) { + final Environment env = environment.environment(); + final ServletEnvironment context = mainContext ? env.servlets() : env.admin(); + + final Set clash = context.addServlet(assetName, + new AssetServlet(resourcePath, uriPath, indexFile, StandardCharsets.UTF_8)) + .addMapping(uriPath + '*'); + + if (clash != null && !clash.isEmpty()) { + throw new IllegalStateException(String.format( + "Assets servlet %s registration clash with already installed servlets on paths: %s", + assetName, Joiner.on(',').join(clash))); + } + + final EnumSet types = EnumSet.of(DispatcherType.REQUEST); + context.addFilter(assetName + "Routing", new SpaRoutingFilter(uriPath, noRedirectRegex)) + .addMappingForServletNames(types, false, assetName); + + logger.info("SPA '{}' for source '{}' registered on uri '{}' in {} context", + assetName, resourcePath, uriPath + '*', mainContext ? "main" : "admin"); + } + + /** + * Register SPA application in main context. + * Note: application names must be unique (when you register multiple SPA applications. + * + * @param name application name (used as servlet name) + * @param resourcePath application resources classpath location (may use slash or dots as separator) + * @param uriPath mapping uri + * @return builder instance for SPA configuration + */ + public static Builder app(final String name, final String resourcePath, final String uriPath) { + return new Builder(true, name, resourcePath, uriPath); + } + + /** + * Register SPA application in admin context. + * Note: application names must be unique (when you register multiple SPA applications. + * + * @param name application name (used as servlet name) + * @param resourcePath path to application resources (classpath) + * @param uriPath mapping uri + * @return builder instance for SPA configuration + */ + public static Builder adminApp(final String name, final String resourcePath, final String uriPath) { + return new Builder(false, name, resourcePath, uriPath); + } + + /** + * Spa bundle builder. + */ + public static class Builder { + private final SpaBundle bundle = new SpaBundle(); + + /** + * Create builder. + * + * @param mainContext true for main context, false for admin + * @param name application name + * @param path resources path + * @param uri mapping url + */ + public Builder(final boolean mainContext, + final String name, + final String path, + final String uri) { + bundle.mainContext = mainContext; + bundle.assetName = checkNotNull(name, "Name is required"); + bundle.uriPath = PathUtils.trailingSlash(uri); + + bundle.resourcePath = PathUtils.normalizeClasspathPath(path); + checkArgument(!SLASH.equals(bundle.resourcePath), "%s is the classpath root", path); + } + + /** + * @param name index file name (by default "index.html") + * @return builder instance + */ + public Builder indexPage(final String name) { + bundle.indexFile = name; + return this; + } + + /** + * Redirect filter will prevent redirection when accept header is not compatible with text/html. + * As this may not be enough, default regex {@link #DEFAULT_PATTERN} is used to prevent redirection in + * some cases. By default it's all common web files (html, css, js) with possible version markers + * (e.g. ?ver=1214324). + *

        + * NOTE: regex is applied with "find", so use ^ or $ to apply boundaries. + * + * @param regex regular expression to prevent redirection to root (prevent when regex matched) + * @return builder instance + */ + public Builder preventRedirectRegex(final String regex) { + bundle.noRedirectRegex = checkNotNull(regex, "Regex can't be null"); + return this; + } + + /** + * @return configured dropwizard bundle instance + */ + public SpaBundle build() { + return bundle; + } + } +} diff --git a/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaBundleState.java b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaBundleState.java new file mode 100644 index 000000000..0f8ea40e4 --- /dev/null +++ b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaBundleState.java @@ -0,0 +1,28 @@ +package ru.vyarus.guicey.spa.filter; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Spa bundle shared state object. + * + * @author Vyacheslav Rusakov + * @since 18.03.2025 + */ +public class SpaBundleState { + private final List usedAssetNames = new ArrayList<>(); + + /** + * Spa bundle's asset name used for filter mapping and so must be unique for all registered bundles. + * + * @param assetName new asset name (for the registering bundle) + */ + public void checkUnique(final String assetName) { + // important because name used for filter mapping + checkArgument(!usedAssetNames.contains(assetName), + "SPA with name '%s' is already registered", assetName); + usedAssetNames.add(assetName); + } +} diff --git a/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaRoutingFilter.java b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaRoutingFilter.java new file mode 100644 index 000000000..1cd793ea0 --- /dev/null +++ b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaRoutingFilter.java @@ -0,0 +1,89 @@ +package ru.vyarus.guicey.spa.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * Filter must be mapped to assets servlet, serving spa application. + * Bypass all calls to servlet, but if servlet returns 404, tries to redirect to application main page. + *

        + * This is important to properly handle html5 client routing (without hashbang). + *

        + * In order to route, filter checks request accept header: if it's compatible with "text/html" - routing is performed. + * If not, 404 error sent. Also, regex pattern is used to prevent routing (for example, for html templates). + * This is important for all other assets, which absence must be indicated. + * + * @author Vyacheslav Rusakov + * @since 02.04.2017 + */ +public class SpaRoutingFilter implements Filter { + + private final String target; + private final Pattern noRedirect; + + /** + * Create SPA filter. + * + * @param target application root + * @param noRedirectRegex non-SPA routes detection regex + */ + public SpaRoutingFilter(final String target, final String noRedirectRegex) { + this.target = target; + noRedirect = Pattern.compile(noRedirectRegex); + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // not needed + } + + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, + final FilterChain chain) throws IOException, ServletException { + + final HttpServletRequest req = (HttpServletRequest) servletRequest; + final HttpServletResponse resp = (HttpServletResponse) servletResponse; + + if (SpaUtils.isRootPage(req.getRequestURI(), target)) { + // direct call for index (no need to redirect) + SpaUtils.noCache(resp); + chain.doFilter(req, resp); + } else { + checkRedirect(req, resp, chain); + } + } + + @Override + public void destroy() { + // not needed + } + + private void checkRedirect(final HttpServletRequest req, + final HttpServletResponse resp, + final FilterChain chain) throws IOException, ServletException { + // wrap request to intercept errors + chain.doFilter(req, resp); + + final int error = resp.getStatus(); + + if (error != HttpServletResponse.SC_NOT_FOUND) { + // nothing to do + return; + } + + if (SpaUtils.isSpaRoute(req, noRedirect)) { + // redirect to root + SpaUtils.doRedirect(req, resp, target); + } + } +} diff --git a/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaUtils.java b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaUtils.java new file mode 100644 index 000000000..b9be63cd5 --- /dev/null +++ b/guicey-spa/src/main/java/ru/vyarus/guicey/spa/filter/SpaUtils.java @@ -0,0 +1,109 @@ +package ru.vyarus.guicey.spa.filter; + +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * Core SPA routes detection logic. + * + * @author Vyacheslav Rusakov + * @since 16.01.2019 + */ +public final class SpaUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(SpaUtils.class); + + private SpaUtils() { + } + + /** + * Note that root path is not the index page, but root mapping path, which will implicitly lead to index page. + * + * @param currentPath current path + * @param rootPath application root path + * @return true if provided path is application root path, false otherwise + */ + public static boolean isRootPage(final String currentPath, final String rootPath) { + final String path = PathUtils.trailingSlash(currentPath); + return path.equals(rootPath); + } + + /** + * Checks if provided request expects html response (by accept header). Did not consider wildcard type + * ({@literal *}/{@literal *})) as html request, because browser request resources (like fonts) with such type. + * Only direct text/html type is recognized (assuming human request). + * + * @param req request instance + * @return true if request expect html, false otherwise + */ + public static boolean isHtmlRequest(final HttpServletRequest req) { + final String accept = req.getHeader(HttpHeaders.ACCEPT); + if (Strings.emptyToNull(accept) != null) { + // accept header could contain multiple mime types + for (String type : accept.split(",")) { + try { + // only exact accept, no wildcard + if (MediaType.valueOf(type).equals(MediaType.TEXT_HTML_TYPE)) { + return true; + } + } catch (Exception ex) { + // ignore errors for better behaviour + LOGGER.debug("Failed to parse media type '" + type + "':", ex.getMessage()); + } + } + } + return false; + } + + /** + * Checks if request could be actually a client side route. SPA route should be a html request + * (by accepted type) and not match to provided pattern (describing non-routing urls). + * + * @param req request instance + * @param noRedirect no-redirect pattern + * @return true if request could be SPA route, false if not + */ + public static boolean isSpaRoute(final HttpServletRequest req, final Pattern noRedirect) { + return isHtmlRequest(req) && !noRedirect.matcher(req.getRequestURI()).find(); + } + + /** + * Applies response header to prevent caching (because SPA page should not be cached). + * + * @param resp response instance + */ + public static void noCache(final HttpServletResponse resp) { + resp.setHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); + } + + /** + * Perform server redirect into root page. Means that current request considered as SPA route and server + * should return index page as response (under the same url) so client could handle url as internal navigation. + *

        + * No cache header is applied to response to prevent index page caching (by this route). + * + * @param req request instance + * @param res response instance + * @param target spa root path + * @throws IOException on error + * @throws ServletException on error + */ + public static void doRedirect(final HttpServletRequest req, + final HttpServletResponse res, + final String target) throws IOException, ServletException { + // remove previous error (404) + res.reset(); + // redirect to root + noCache(res); + req.getRequestDispatcher(target).forward(req, res); + } +} diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AbstractTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AbstractTest.groovy new file mode 100644 index 000000000..a24d67570 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AbstractTest.groovy @@ -0,0 +1,50 @@ +package ru.vyarus.guicey.spa + +import org.apache.commons.text.StringEscapeUtils +import ru.vyarus.dropwizard.guice.test.ClientSupport +import spock.lang.Specification + +import jakarta.ws.rs.client.WebTarget +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 29.11.2019 + */ +abstract class AbstractTest extends Specification { + + // default builder for text/html type (user call simulation) + ClientSupport client + + void setup(ClientSupport client) { + this.client = client + } + + // shortcut to return body + String get(String url) { + call(main(), url) + } + + String adminGet(String url) { + call(admin(), url) + } + + protected WebTarget main() { + return client.target('http://localhost:8080') + } + + protected WebTarget admin() { + return client.target('http://localhost:8081') + } + + private String call(WebTarget http, String path) { + Response res = http.path(path).request(MediaType.TEXT_HTML).get() + if (res.status == 404) { + throw new FileNotFoundException() + } else if (res.status != 200) { + throw new IOException("status: ${res.status}") + } + return StringEscapeUtils.unescapeHtml4(res.readEntity(String)) + } +} diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AdminSpaMappingTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AdminSpaMappingTest.groovy new file mode 100644 index 000000000..02b562a2b --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/AdminSpaMappingTest.groovy @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@TestDropwizardApp(App) +class AdminSpaMappingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = adminGet("/app") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = adminGet("/app/some") + then: "error" + res.contains("Sample page") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle.adminApp("app", "/app", "/app").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomIndexTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomIndexTest.groovy new file mode 100644 index 000000000..1d34b7903 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomIndexTest.groovy @@ -0,0 +1,41 @@ +package ru.vyarus.guicey.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class CustomIndexTest extends AbstractTest { + + def "Check custom index"() { + + when: "accessing app" + String res = get("/") + then: "index page" + res.contains("Other index") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle + .app("app", "/app", "/") + .indexPage("idx.htm") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomRegexTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomRegexTest.groovy new file mode 100644 index 000000000..c5737fb11 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/CustomRegexTest.groovy @@ -0,0 +1,46 @@ +package ru.vyarus.guicey.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class CustomRegexTest extends AbstractTest { + + def "Check custom regex"() { + + when: "accessing html" + String res = get("/some/some.html") + then: "index page" + res.contains("Sample page") + + when: "accessing js" + get("/some/some.js") + then: "index page" + thrown(FileNotFoundException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle + .app("app", "/app", "/") + .preventRedirectRegex("\\.js\$") + .build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/FlatAdminMappingTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/FlatAdminMappingTest.groovy new file mode 100644 index 000000000..fd7695c36 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/FlatAdminMappingTest.groovy @@ -0,0 +1,44 @@ +package ru.vyarus.guicey.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@TestDropwizardApp(value = App, config = 'src/test/resources/flat.yml') +class FlatAdminMappingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = get("/admin/app") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = get("/admin/app/some") + then: "error" + res.contains("Sample page") + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle.adminApp("app", "/app", "/app").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/MultipleBundlesMappingTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/MultipleBundlesMappingTest.groovy new file mode 100644 index 000000000..1b2acaf8c --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/MultipleBundlesMappingTest.groovy @@ -0,0 +1,103 @@ +package ru.vyarus.guicey.spa + +import com.google.common.net.HttpHeaders +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class MultipleBundlesMappingTest extends AbstractTest { + + def "Check spa mappings"() { + + when: "first" + String res = get("/1") + then: "index page" + res.contains("Sample page") + + when: "second" + res = get("/2") + then: "index page" + res.contains("Sample page") + + when: "admin first" + res = adminGet("/a1") + then: "index page" + res.contains("Sample page") + + when: "admin second" + res = adminGet("/a2") + then: "index page" + res.contains("Sample page") + + + when: "accessing not existing page" + res = get("/2/some/") + then: "error" + res.contains("Sample page") + + when: "accessing not existing admin page" + res = adminGet("/a2/some/") + then: "error" + res.contains("Sample page") + } + + def "Check cache header"() { + when: "calling index" + def res = client.targetApp('/1').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "calling index 1" + res = client.targetApp('/2').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect" + res = client.targetApp('/1/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect 2" + res = client.targetApp('/2/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "direct index page" + res = client.targetApp('/1/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + + when: "direct index page 2" + res = client.targetApp('/2/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + SpaBundle.app("app", "/app", "/1").build(), + SpaBundle.app("app2", "/app", "/2").build(), + SpaBundle.adminApp("aapp1", "/app", "/a1").build(), + SpaBundle.adminApp("aapp2", "/app", "/a2").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/PackageResourceDeclarationTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/PackageResourceDeclarationTest.groovy new file mode 100644 index 000000000..cc5b14c43 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/PackageResourceDeclarationTest.groovy @@ -0,0 +1,39 @@ +package ru.vyarus.guicey.spa + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +/** + * @author Vyacheslav Rusakov + * @since 05.12.2019 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class PackageResourceDeclarationTest extends AbstractTest { + + def "Check resources mapped"() { + + when: "accessing resource" + String res = get("/some.css") + then: "style" + res == "/* styles */" + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle.app("app", "app.css", "/").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/SpaMappingTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/SpaMappingTest.groovy new file mode 100644 index 000000000..72038529b --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/SpaMappingTest.groovy @@ -0,0 +1,109 @@ +package ru.vyarus.guicey.spa + +import com.google.common.net.HttpHeaders +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.ClientSupport +import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp + +import jakarta.ws.rs.core.MediaType + +/** + * @author Vyacheslav Rusakov + * @since 02.04.2017 + */ +@TestDropwizardApp(value = App, restMapping = "/rest/*") +class SpaMappingTest extends AbstractTest { + + def "Check spa mapped"() { + + when: "accessing app" + String res = get("/") + then: "index page" + res.contains("Sample page") + + when: "accessing not existing page" + res = get("/some/") + then: "ok" + res.contains("Sample page") + + when: "accessing not existing resource" + get("/some.html") + then: "error" + thrown(FileNotFoundException) + } + + def "Check no cache header"(ClientSupport client) { + + when: "calling index" + def res = client.targetApp('/').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "force redirect" + res = client.targetApp('/some').request(MediaType.TEXT_HTML).get() + then: "cache disabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == 'must-revalidate,no-cache,no-store' + + when: "direct index page" + res = client.targetApp('/index.html').request(MediaType.TEXT_HTML).get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + + when: "resource" + res = client.targetApp('/css/some.css').request().get() + then: "cache enabled" + res.getHeaderString(HttpHeaders.CACHE_CONTROL) == null + } + + def "Chck different mime type"() { + + when: "calling with html type" + def res = client.targetApp('/some').request(MediaType.TEXT_HTML).get() + then: "redirect" + res.status == 200 + + when: "calling with text type" + res = client.targetApp('/some').request(MediaType.TEXT_PLAIN).get() + then: "no redirect" + res.status == 404 + + when: "calling with unknown content type" + res = client.targetApp('/some').request("abrakadabra").get() + then: "no redirect" + res.status == 404 + + when: "calling with empty type" + res = client.targetApp('/some').request(" ").get() + then: "empty response type not allowed" + res.status == 404 + + } + + def "Check non 404 error"() { + + when: "calling for cached content" + def res = client.targetApp('/index.html').request(MediaType.TEXT_HTML) + .header('If-Modified-Since', 'Wed, 21 Oct 2215 07:28:00 GMT').get() + then: "cache disabled" + res.status == 304 + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(SpaBundle.app("app", "app", "/").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/MappingClashTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/MappingClashTest.groovy new file mode 100644 index 000000000..667fcb567 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/MappingClashTest.groovy @@ -0,0 +1,55 @@ +package ru.vyarus.guicey.spa.err + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.extension.ExtendWith +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.spa.SpaBundle +import spock.lang.Specification +import uk.org.webcompere.systemstubs.jupiter.SystemStub +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension +import uk.org.webcompere.systemstubs.stream.SystemErr + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +@ExtendWith(SystemStubsExtension) +class MappingClashTest extends Specification { + + @SystemStub + SystemErr err + + def "Check uri paths clash"() { + + when: "starting app" + Assertions.assertThrows(RuntimeException.class, () -> new App().run(['server'] as String[])) + + then: "error" + err.text.contains("Assets servlet app2 registration clash with already installed servlets on paths: /app/*") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + SpaBundle.app("app1", "/app", "/app").build(), + SpaBundle.app("app2", "/app", "/app").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + + @Override + protected void onFatalError(Throwable t) { + throw new RuntimeException(t) + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/SameNameTest.groovy b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/SameNameTest.groovy new file mode 100644 index 000000000..8831e9606 --- /dev/null +++ b/guicey-spa/src/test/groovy/ru/vyarus/guicey/spa/err/SameNameTest.groovy @@ -0,0 +1,41 @@ +package ru.vyarus.guicey.spa.err + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.guicey.spa.SpaBundle +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 05.04.2017 + */ +class SameNameTest extends Specification { + + def "Check duplicate spa names"() { + + when: "starting app" + new App().run(['server'] as String[]) + then: "error" + def ex = thrown(IllegalArgumentException) + ex.message.contains("SPA with name 'app' is already registered") + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles( + SpaBundle.app("app", "/app", "/1").build(), + SpaBundle.app("app", "/app", "/2").build()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} \ No newline at end of file diff --git a/guicey-spa/src/test/resources/app/css/some.css b/guicey-spa/src/test/resources/app/css/some.css new file mode 100644 index 000000000..e30dc7105 --- /dev/null +++ b/guicey-spa/src/test/resources/app/css/some.css @@ -0,0 +1 @@ +/* styles */ \ No newline at end of file diff --git a/guicey-spa/src/test/resources/app/idx.htm b/guicey-spa/src/test/resources/app/idx.htm new file mode 100644 index 000000000..9ccc4bfc5 --- /dev/null +++ b/guicey-spa/src/test/resources/app/idx.htm @@ -0,0 +1,14 @@ + + + + + Other index + + + +

        + Other index +
        + + + \ No newline at end of file diff --git a/guicey-spa/src/test/resources/app/index.html b/guicey-spa/src/test/resources/app/index.html new file mode 100644 index 000000000..90f90a3cf --- /dev/null +++ b/guicey-spa/src/test/resources/app/index.html @@ -0,0 +1,14 @@ + + + + + Sample page + + + +
        + Sample page +
        + + + \ No newline at end of file diff --git a/guicey-spa/src/test/resources/app/js/some.js b/guicey-spa/src/test/resources/app/js/some.js new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/guicey-spa/src/test/resources/app/js/some.js @@ -0,0 +1 @@ + diff --git a/guicey-spa/src/test/resources/flat.yml b/guicey-spa/src/test/resources/flat.yml new file mode 100644 index 000000000..6afe72832 --- /dev/null +++ b/guicey-spa/src/test/resources/flat.yml @@ -0,0 +1,14 @@ +server: + rootPath: '/rest/*' + + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 + + requestLog: + appenders: + - type: console + timeZone: Asia/Novosibirsk \ No newline at end of file diff --git a/guicey-test-junit4/README.md b/guicey-test-junit4/README.md new file mode 100644 index 000000000..d1a2fb73e --- /dev/null +++ b/guicey-test-junit4/README.md @@ -0,0 +1,241 @@ +# Junit 4 + +### About + +Junit 4 test support + +NOTE: Module was extracted from guicey core. Package remains the same to simplify migration (only additional dependency would be required). +Also, deprecation marks removed from rules to reduce warnings. + +DEPRECATED because dropwizard deprecated its junit4 rules (dropwizard junit4 support was extracted into [separate module](https://github.com/dropwizard/dropwizard-testing-junit4)). +Consider [migration to JUnit 5](#migrating-to-junit-5) + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-test-junit4 + {guicey.version} + test + +``` + +Gradle: + +```groovy +testImplementation 'ru.vyarus.guicey:guicey-test-junit4:{guicey.version}' +``` + +Omit version if guicey BOM used. + +#### With junit 5 + +OR you can use it with junit 5 vintage engine (assume BOM used for version management): + +```groovy +testImplementation 'ru.vyarus.guicey:guicey-test-junit4' +testImplementation 'org.junit.jupiter:junit-jupiter-api' +testRuntimeOnly 'org.junit.jupiter:junit-jupiter' +testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' +``` + +This way all existing junit 4 tests would work and new tests could use junit 5 extensions. + +### Usage + +Provided rules: + +* `GuiceyAppRule` - lightweight integration tests (guice only) +* `GuiceyHooksRule` - test-specific application modifications +* `StartupErrorRule` - helper for testing failed application startup + +#### Testing core logic + +For integration testing of guice specific logic you can use `GuiceyAppRule`. It works almost like +[DropwizardAppRule](https://github.com/dropwizard/dropwizard-testing-junit4), +but *doesn't start jetty* (and so jersey and guice web modules will not be initialized). +Managed and lifecycle objects supported. + +```java +public class MyTest { + + @Rule + GuiceyAppRule RULE = new GuiceyAppRule<>(MyApplication.class, "path/to/configuration.yaml"); + + public void testSomething() { + RULE.getBean(MyService.class).doSomething(); + ... + } +} +``` + +As with dropwizard rule, configuration is optional + +```java +new GuiceyAppRule<>(MyApplication.class, null) +``` + +#### Testing web logic + +For web component tests (servlets, filters, resources) use +[DropwizardAppRule](https://github.com/dropwizard/dropwizard-testing-junit4#usage). + +To access guice beans use injector lookup: + +```java +InjectorLookup.getInstance(RULE.getApplication(), MyService.class).get(); +``` + +#### Customizing guicey configuration + +Guicey [provides a way](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/hooks/) to modify its configuration in tests. +You can apply configuration hook using rule: + +```java +// there may be exact class instead of lambda +new GuiceyHooksRule((builder) -> builder.modules(...)) +``` + +To use it with `DropwizardAppRule` or `GuiceyAppRule` you will have to apply explicit order: + +```java +static GuiceyAppRule RULE = new GuiceyAppRule(App.class, null); +@ClassRule +public static RuleChain chain = RuleChain + .outerRule(new GuiceyHooksRule((builder) -> builder.modules(...))) + .around(RULE); +``` + +ATTENTION: +RuleChain is required because rules execution order is not guaranteed and +configuration rule must obviously be executed before application rule. + +If you need to declare configurations common for all tests then declare rule instance +in base test class and use it in chain (at each test): + +```java +public class BaseTest { + // IMPORTANT no @ClassRule annotation here! + static GuiceyHooksRule BASE = new GuiceyHooksRule((builder) -> builder.modules(...)) + } + + public class SomeTest extends BaseTest { + static GuiceyAppRule RULE = new GuiceyAppRule(App.class, null); + @ClassRule + public static RuleChain chain = RuleChain + .outerRule(BASE) + // optional test-specific staff + .around(new GuiceyHooksRule((builder) -> builder.modules(...)) + .around(RULE); + } +``` + +WARNING +Don't use configuration rule with spock because it will not work. Use special spock extension instead. + +#### Access guice beans + +When using `DropwizardAppRule` the only way to obtain guice managed beans is through: + +```java +InjectorLookup.getInjector(RULE.getApplication()).getBean(MyService.class); +``` + +Also, the following trick may be used to inject test fields: + +```java +public class MyTest { + + @ClassRule + static DropwizardAppRule RULE = ... + + @Inject MyService service; + @Inject MyOtherService otherService; + + @Before + public void setUp() { + InjectorLookup.get(RULE.getApplication()).get().injectMemebers(this) + } +} +``` + +#### Testing startup errors + +If exception occur on startup dropwizard will call `System.exit(1)` instead of throwing exception (as it was before 1.1.0). +System exit could be intercepted with [system rules](http://stefanbirkner.github.io/system-rules/index.html). + +Special rule provided to simplify work with system rules: `StartupErrorRule`. +It's a combination of exit and out/err outputs interception rules. + +```java +public class MyErrTest { + + @Rule + public StartupErrorRule RULE = StartupErrorRule.create(); + + public void testSomething() { + new MyErrApp().main('server'); + } +} +``` + +This test will pass only if application will throw exception during startup. + +In junit it is impossible to apply checks after exit statement, so such checks +must be registered as a special callback: + +```java +public class MyErrTest { + + @Rule + public StartupErrorRule RULE = StartupErrorRule.create((out, err) -> { + Assert.assertTrue(out.contains("some log line")); + Assert.assertTrue(err.contains("expected exception message")); + }); + + public void testSomething() { + new MyErrApp().main('server'); + } +} +``` + +Note that err will contain full exception stack trace and so you can check exception type too +by using contains statement like above. + +Check callback(s) may be added after rule creation: + +```java +@Rule +public StartupErrorRule RULE = StartupErrorRule.create(); + +public void testSomething() throws Exception { + RULE.checkAfterExit((out, err) -> { + Assert.assertTrue(err.contains("expected exception message")); + }); + ... +} +``` + +Multiple check callbacks may be registered (even if the first one was registered in rule's +create call). + +!!! note "" +Rule works a bit differently [with spock 1](../guicey-test-spock/README.md#dropwizard-startup-error). + +### Migrating to JUnit 5 + +* Instead of `GuiceyAppRule` use [@TestGuiceyApp](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#testguiceyapp) extension. +* Instead of `DropwizardAppRule` use [@TestDropwizardApp](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#testdropwizardapp) extension. +* `GuiceyHooksRule` can be substituted with hooks declaration [in extensions](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#application-test-modification) or as [test fields](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#hook-fields) +* Instead of `StartupErrorRule` use [system-stubs](https://github.com/webcompere/system-stubs) - the successor of system rules + +In essence: + +* Use annotations instead of rules (and forget about RuleChain difficulties) +* Test fields injection will work out of the box, so no need for additional hacks +* JUnit 5 propose [parameter injection](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#parameter-injection), which may be not common at first, but its actually very handy + +Also, there is a pre-configured [http client](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#client) suitable for calling test application urls (or any other general url). \ No newline at end of file diff --git a/guicey-test-junit4/build.gradle b/guicey-test-junit4/build.gradle new file mode 100644 index 000000000..e67261976 --- /dev/null +++ b/guicey-test-junit4/build.gradle @@ -0,0 +1,12 @@ +description = 'JUnit 4 test support' + +dependencies { + implementation ('com.github.stefanbirkner:system-rules:1.19.0') { + exclude group: 'junit', module: 'junit-dep' + } + + implementation 'io.dropwizard.modules:dropwizard-testing-junit4:5.0.0' + implementation 'junit:junit' + + testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' +} \ No newline at end of file diff --git a/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyAppRule.java b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyAppRule.java new file mode 100644 index 000000000..f8424fb20 --- /dev/null +++ b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyAppRule.java @@ -0,0 +1,175 @@ +package ru.vyarus.dropwizard.guice.test; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.ConfigOverride; +import net.sourceforge.argparse4j.inf.Namespace; +import org.junit.rules.ExternalResource; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; + +import jakarta.annotation.Nullable; +import ru.vyarus.dropwizard.guice.module.installer.util.InstanceUtils; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; + +/** + * A JUnit rule for starting and stopping your guice application at the start and end of a test class. + *

        + * By default, the {@link Application} will be constructed using reflection to invoke the nullary + * constructor. If your application does not provide a public nullary constructor, you will need to + * override the {@link #newApplication()} method to provide your application instance(s).

        + *

        Based on {@link io.dropwizard.testing.junit.DropwizardAppRule}, but doesn't start jetty and as a consequence + * jersey and guice web modules not initialized. Emulates managed objects lifecycle.

        + *

        Suppose to be used for testing internal services business logic as lightweight alternative for + * dropwizard rule.

        + * + * @param configuration type + * @author Vyacheslav Rusakov + * @see ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp for junit 5 alterantive + * @since 23.10.2014 + */ +public class GuiceyAppRule extends ExternalResource { + + private final Class> applicationClass; + private final String configPath; + private final List configOverrides; + + private C configuration; + private Application application; + private Environment environment; + private TestCommand command; + + /** + * Create app rule. + * + * @param applicationClass application class + * @param configPath configuration path + * @param configOverrides configuration overrides + */ + public GuiceyAppRule(final Class> applicationClass, + @Nullable final String configPath, + final ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = configPath; + this.configOverrides = Arrays.asList(configOverrides); + } + + /** + * @return configuration object + */ + public C getConfiguration() { + return configuration; + } + + /** + * @param application type + * @return application instance + */ + @SuppressWarnings("unchecked") + public > A getApplication() { + return (A) application; + } + + /** + * @return environment object + */ + public Environment getEnvironment() { + return environment; + } + + /** + * @return guice injector + */ + public Injector getInjector() { + return InjectorLookup.getInjector(application).get(); + } + + /** + * @param type bean type + * @param bean type + * @return bean instance + */ + public T getBean(final Class type) { + return getInjector().getInstance(type); + } + + /** + * @return new application instance + */ + protected Application newApplication() { + try { + return InstanceUtils.create(applicationClass); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate application", e); + } + } + + @Override + protected void before() throws Throwable { + for (ConfigOverride configOverride : configOverrides) { + configOverride.addToSystemProperties(); + } + startIfRequired(); + } + + @Override + protected void after() { + resetConfigOverrides(); + command.stop(); + command = null; + } + + private void startIfRequired() { + if (command != null) { + return; + } + + try { + application = newApplication(); + + final Bootstrap bootstrap = new Bootstrap<>(application) { + @Override + public void run(final C configuration, final Environment environment) throws Exception { + GuiceyAppRule.this.configuration = configuration; + GuiceyAppRule.this.environment = environment; + super.run(configuration, environment); + } + }; + + application.initialize(bootstrap); + + startCommand(bootstrap); + + } catch (Exception e) { + throw new IllegalStateException("Failed to start test environment", e); + } + } + + private void startCommand(final Bootstrap bootstrap) throws Exception { + command = new TestCommand<>(application); + + final ImmutableMap.Builder file = ImmutableMap.builder(); + if (!Strings.isNullOrEmpty(configPath)) { + file.put("file", configPath); + } + final Namespace namespace = new Namespace(file.build()); + + command.run(bootstrap, namespace); + } + + private void resetConfigOverrides() { + for (final Enumeration props = System.getProperties().propertyNames(); props.hasMoreElements();) { + final String keyString = (String) props.nextElement(); + if (keyString.startsWith("dw.")) { + System.clearProperty(keyString); + } + } + } +} diff --git a/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyHooksRule.java b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyHooksRule.java new file mode 100644 index 000000000..549d7d9f8 --- /dev/null +++ b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyHooksRule.java @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.test; + +import org.junit.rules.ExternalResource; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.util.Arrays; +import java.util.List; + +/** + * Junit rule for changing application configuration (remove some components or register test specific (e.g. mocks)). + * Supposed to be used in conjunction with {@link io.dropwizard.testing.junit.DropwizardAppRule} or + * {@link GuiceyAppRule}. Must be used ONLY with {@link org.junit.rules.RuleChain} because normally rules order + * is not predictable: + *
        {@code static GuiceyAppRule RULE = new GuiceyAppRule(App.class, null);
        + *    {@literal @}ClassRule
        + *    public static RuleChain chain = RuleChain
        + *            .outerRule(new GuiceyHooksRule((builder) -> builder.modules(...)))
        + *            .around(RULE);
        + * }
        + * To declare common extensions for all tests, declare common rule in test class (without {@code @ClassRule} + * annotation!) and use it in chain: + *
        {@code public class BaseTest {
        + *         static GuiceyHooksRule BASE = new GuiceyHooksRule((builder) -> builder.modules(...))
        + *     }
        + *
        + *     public class SomeTest extends BaseTest {
        + *         static GuiceyAppRule RULE = new GuiceyAppRule(App.class, null);
        + *         {@literal @}ClassRule
        + *         public static RuleChain chain = RuleChain
        + *            .outerRule(BASE)
        + *            .around(new GuiceyHooksRule((builder) -> builder.modules(...)) // optional test-specific staff
        + *            .around(RULE);
        + *     }
        + * }
        + *

        + * IMPORTANT: rule will not work with spock extensions (because of lifecycle specifics)! Use + * {@code UseGuiceyHooks} or new {@code hooks} attribute in + * {@code ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp} or + * {@code ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp} instead. + *

        + * Rule is thread safe: it is assumed that rule will be applied at the same thread as test application initialization. + * + * @author Vyacheslav Rusakov + * @since 11.04.2018 + */ +public class GuiceyHooksRule extends ExternalResource { + + private final List hooks; + + /** + * Create hook rule. + * + * @param hooks hooks + */ + public GuiceyHooksRule(final GuiceyConfigurationHook... hooks) { + this.hooks = Arrays.asList(hooks); + } + + @Override + protected void before() throws Throwable { + hooks.forEach(GuiceyConfigurationHook::register); + } + + @Override + protected void after() { + // normally reset is not required, but called to avoid possible state for some failed cases + ConfigurationHooksSupport.reset(); + } +} diff --git a/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/StartupErrorRule.java b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/StartupErrorRule.java new file mode 100644 index 000000000..807af3c43 --- /dev/null +++ b/guicey-test-junit4/src/main/java/ru/vyarus/dropwizard/guice/test/StartupErrorRule.java @@ -0,0 +1,139 @@ +package ru.vyarus.dropwizard.guice.test; + +import org.junit.contrib.java.lang.system.ExpectedSystemExit; +import org.junit.contrib.java.lang.system.SystemErrRule; +import org.junit.contrib.java.lang.system.SystemOutRule; +import org.junit.contrib.java.lang.system.internal.CheckExitCalled; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Dropwizard exit with code 1 in case of exception during command run. In order to test error situations + * system exit must be prevented. + * Rule use system rules to intercept + * exit and output streams (system.out, system.err). + *

        + * Extra dependency 'com.github.stefanbirkner:system-rules:1.16.0' is required. + *

        + * In spock tests, 'then' section must contain {@code rule.indicatorExceptionType} (or {@code thrown(CheckExitCalled)}) + * as rule can't intercept this exception. + * 'then' section can be used for assertions after system exit and so no need to register custom callback in the rule. + *

        + * For Junit 5 there is no direct alternative, but there are options: + *

          + *
        • For system exit capturing you can use https://github.com/tginsberg/junit5-system-exit
        • + *
        • For system streams capturing there is no alternatives, but in any case it is impossible to do for parallel + * tests
        • + *
        + * + * @author Vyacheslav Rusakov + * @since 16.03.2017 + */ +public final class StartupErrorRule implements TestRule { + private final ExpectedSystemExit exit = ExpectedSystemExit.none(); + private final SystemErrRule systemErr = new SystemErrRule(); + private final SystemOutRule systemOut = new SystemOutRule(); + + private StartupErrorRule() { + exit.expectSystemExitWithStatus(1); + systemErr.enableLog(); + systemOut.enableLog(); + } + + /** + * Use with spock tests or when no assertions required after system exit call. + * + * @return rule instance + */ + public static StartupErrorRule create() { + return new StartupErrorRule(); + } + + /** + * This is useful for junit tests, because there is no other way to check anything after exit call. + * In spock, 'then' section is always called and so may be used for assertions. + * + * @param check assertion callback to execute after exit call + * @return activated rule + */ + public static StartupErrorRule create(final AfterExitAssertion check) { + return create().checkAfterExit(check); + } + + /** + * In junit it is impossible to use assertion lines after {@code System.exit()} call. This method will register + * a custom check callback to validate state after system exit call. + * May be called multiple times. + *

        + * NOTE: in spock there is no need for it, because 'then' section will be called. + * + * @param check assertion command + * @return rule instance for chained calls + * @see ExpectedSystemExit#checkAssertionAfterwards(org.junit.contrib.java.lang.system.Assertion) + */ + public StartupErrorRule checkAfterExit(final AfterExitAssertion check) { + exit.checkAssertionAfterwards(() -> check.check(getOutput(), getError())); + return this; + } + + @Override + public Statement apply(final Statement base, final Description description) { + + // sys.out -> sys.err -> system exit + return systemOut.apply( + systemErr.apply( + exit.apply(base, description), + description), + description); + } + + /** + * NOTE: useful only for spock tests. + * + * @return content of system.out or empty string + */ + public String getOutput() { + return clearString(systemOut.getLog()); + } + + /** + * Dropwizard exception will be presented here. + *

        + * NOTE: useful only for spock tests. + * + * @return content of system.err or empty string + */ + public String getError() { + return clearString(systemErr.getLog()); + } + + /** + * Useful only for spock tests because in junit rule can intercept this exception implicitly. + * + * @return type of exception thrown when exit called + */ + public Class getIndicatorExceptionType() { + return CheckExitCalled.class; + } + + private String clearString(final String message) { + return message.trim().replaceAll("\r", ""); + } + + /** + * Interface implementation may be registered to check assertions after system exit. + */ + @FunctionalInterface + public interface AfterExitAssertion { + + /** + * Called after system exit call to perform custom assertions. + * + * @param out output stream content or empty string + * @param err error stream content (exception logged there) + * @throws Exception in case of error + */ + void check(String out, String err) throws Exception; + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy similarity index 90% rename from src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy rename to guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy index 2489c6c8b..ee7ade083 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/AbstractTest.groovy @@ -6,10 +6,10 @@ import io.dropwizard.jersey.setup.JerseyEnvironment import io.dropwizard.jetty.MutableServletContextHandler import io.dropwizard.jetty.setup.ServletEnvironment import io.dropwizard.lifecycle.setup.LifecycleEnvironment -import io.dropwizard.logging.BootstrapLogging -import io.dropwizard.logging.LoggingUtil -import io.dropwizard.setup.AdminEnvironment -import io.dropwizard.setup.Environment +import io.dropwizard.logging.common.BootstrapLogging +import io.dropwizard.logging.common.LoggingUtil +import io.dropwizard.core.setup.AdminEnvironment +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.bundle.lookup.PropertyBundleLookup import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState @@ -18,8 +18,8 @@ import ru.vyarus.dropwizard.guice.support.util.GuiceRestrictedConfigBundle import ru.vyarus.dropwizard.guice.test.EnableHook import spock.lang.Specification -import javax.servlet.FilterRegistration -import javax.servlet.ServletRegistration +import jakarta.servlet.FilterRegistration +import jakarta.servlet.ServletRegistration /** * Base class for tests. diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/StartErrorTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/StartErrorTest.groovy new file mode 100644 index 000000000..f3ee7e6a6 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/StartErrorTest.groovy @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice + +import com.google.inject.AbstractModule +import com.google.inject.name.Named +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.junit.Rule +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import ru.vyarus.dropwizard.guice.support.feature.DummyCommand +import ru.vyarus.dropwizard.guice.test.StartupErrorRule + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 24.11.2015 + */ +class StartErrorTest extends AbstractTest { + + @Rule + StartupErrorRule rule = StartupErrorRule.create() + + def "Check application exit on injector error"() { + + when: + new ErrorApplication().main(['server', 'src/test/resources/ru/vyarus/dropwizard/guice/config.yml'] as String[]) + + then: 'guice exception thrown' + thrown(rule.indicatorExceptionType) + // java 9 and above use quotes in annotations (@com.google.inject.name.Named(value="unknown")) while previous versions did not + rule.error.replace('"', '').contains( + "[Guice/JitDisabled]: Explicit bindings are required and String annotated with @Named") + } + + static class ErrorApplication extends Application { + static void main(String[] args) { + new ErrorApplication().run(args) + } + + @Override + void initialize(Bootstrap bootstrap) { + TEST_HOOK.register() + bootstrap.addBundle(GuiceBundle.builder() + .modules(new ErrorModule()) + .build() + ); + bootstrap.addCommand(new DummyCommand(bootstrap.getApplication())) + } + + @Override + void run(TestConfiguration configuration, Environment environment) throws Exception { + } + } + + static class ErrorModule extends AbstractModule { + + @Override + protected void configure() { + bind(ErrorService).asEagerSingleton() + } + } + + static class ErrorService { + + @Inject + ErrorService(@Named('unknown') String unknown) { + } + } +} \ No newline at end of file diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy new file mode 100644 index 000000000..ebdeb6c97 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.support + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.support.util.BindModule + +/** + * Example of automatic configuration: installers, beans and commands searched automatically. + * @author Vyacheslav Rusakov + * @since 01.09.2014 + */ +class AutoScanApplication extends Application { + + public static void main(String[] args) { + new AutoScanApplication().run(args) + } + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig("ru.vyarus.dropwizard.guice.support.feature") + .searchCommands() + .modules(new BindModule(Service)) + .build() + ); + } + + @Override + void run(TestConfiguration configuration, Environment environment) throws Exception { + } + + static class Service {} +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java similarity index 91% rename from src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java rename to guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java index e7d05fec2..e460d86d5 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java @@ -1,7 +1,7 @@ package ru.vyarus.dropwizard.guice.support; import com.fasterxml.jackson.annotation.JsonProperty; -import io.dropwizard.Configuration; +import io.dropwizard.core.Configuration; /** * Groovy class can't be used anymore, because jackson 2.5 is very sensible for additional methods. diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.groovy new file mode 100644 index 000000000..2c877ae5d --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyCommand.groovy @@ -0,0 +1,27 @@ +package ru.vyarus.dropwizard.guice.support.feature + +import com.google.inject.Inject +import io.dropwizard.core.Application +import io.dropwizard.core.cli.EnvironmentCommand +import io.dropwizard.core.setup.Environment +import net.sourceforge.argparse4j.inf.Namespace +import ru.vyarus.dropwizard.guice.support.TestConfiguration + +/** + * @author Vyacheslav Rusakov + * @since 03.09.2014 + */ +class DummyCommand extends EnvironmentCommand { + + @Inject + DummyService service + + DummyCommand(Application app) { + super(app, "sample", "sample command") + } + + @Override + protected void run(Environment environment, Namespace namespace, TestConfiguration configuration) throws Exception { + println "I'm alive! ${service.hey()}" + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy new file mode 100644 index 000000000..16d1c310f --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy @@ -0,0 +1,15 @@ +package ru.vyarus.dropwizard.guice.support.feature + +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton + +/** + * @author Vyacheslav Rusakov + * @since 03.09.2014 + */ +@EagerSingleton +class DummyService { + + public String hey() { + 'hey!' + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy new file mode 100644 index 000000000..cb90433a0 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.support.util + +import com.google.inject.AbstractModule + +/** + * Module used to explicitly bind services normally resolved by JIT. + * + * @author Vyacheslav Rusakov + * @since 19.06.2016 + */ +class BindModule extends AbstractModule { + private Class[] types + + BindModule(Class... types) { + this.types = types + } + + @Override + protected void configure() { + types.each { + bind(it) + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy new file mode 100644 index 000000000..0d88a6ad2 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/GuiceRestrictedConfigBundle.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.support.util + +import com.google.inject.AbstractModule +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle + +/** + * Applies guice restrictions for tests to make sure guicey is able to work in such conditions. + * + * @author Vyacheslav Rusakov + * @since 19.06.2016 + */ +class GuiceRestrictedConfigBundle implements GuiceyBundle { + + @Override + void initialize(GuiceyBootstrap bootstrap) { + bootstrap.modules(new GRestrictModule()) + } + + static class GRestrictModule extends AbstractModule { + @Override + protected void configure() { + binder().disableCircularProxies() + binder().requireExactBindingAnnotations() + binder().requireExplicitBindings() + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleConfigOverrideTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleConfigOverrideTest.groovy new file mode 100644 index 000000000..a04f1111f --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleConfigOverrideTest.groovy @@ -0,0 +1,47 @@ +package ru.vyarus.dropwizard.guice.test + + +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import spock.lang.Specification + +import static io.dropwizard.testing.ConfigOverride.config + +/** + * @author Brian Wehrle + * @since 05.03.2021 + */ +class GuiceyRuleConfigOverrideTest extends Specification { + + private final static String CONFIG_PROPERTY = "server.type"; + private final static String CONFIG_PROPERTY_VALUE = "default"; + private final static String PROPERTY_PREFIX = "dw"; + + @SuppressWarnings('GrDeprecatedAPIUsage') + def "Test config override not applied in init"() { + GuiceyAppRule guiceyAppRule = new GuiceyAppRule<>(AutoScanApplication, + null, + config(CONFIG_PROPERTY, CONFIG_PROPERTY_VALUE)); + + expect: "config override not applied after init" + !isPropertyPresent() + } + + @SuppressWarnings('GrDeprecatedAPIUsage') + def "Test config override applied in before"() { + GuiceyAppRule guiceyAppRule = new GuiceyAppRule<>(AutoScanApplication, + null, + config(CONFIG_PROPERTY, CONFIG_PROPERTY_VALUE)); + + expect: "config override applied after before()" + guiceyAppRule.before() + isPropertyPresent() + + cleanup: + guiceyAppRule.after() + } + + def static isPropertyPresent() { + Object value = System.getProperties().get(PROPERTY_PREFIX + "." + CONFIG_PROPERTY); + return value.toString().equals(CONFIG_PROPERTY_VALUE); + } +} \ No newline at end of file diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleFailedStartTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleFailedStartTest.groovy new file mode 100644 index 000000000..3af494cbe --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleFailedStartTest.groovy @@ -0,0 +1,56 @@ +package ru.vyarus.dropwizard.guice.test + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 11.07.2016 + */ +class GuiceyRuleFailedStartTest extends Specification { + + def "Check failed creation"() { + + when: "start rule for failed app" + new GuiceyAppRule(FailedApp, null).before() + then: "error thrown" + def ex = thrown(IllegalStateException) + ex.message == "Failed to start test environment" + ex.cause.message == "Failed to instantiate application" + ex.cause.cause.message == "Oops" + } + + def "Check failed startup"() { + + when: "start rule for failed app" + new GuiceyAppRule(FailedStartApp, null).before() + then: "error thrown" + def ex = thrown(IllegalStateException) + ex.message == "Failed to start test environment" + ex.cause.message == "Oops" + } + + static class FailedApp extends Application { + FailedApp() { + throw new IllegalStateException("Oops") + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class FailedStartApp extends Application { + @Override + void initialize(Bootstrap bootstrap) { + throw new IllegalStateException("Oops") + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleTest.groovy new file mode 100644 index 000000000..cb9a8e4c5 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyRuleTest.groovy @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Key +import io.dropwizard.core.setup.Environment +import org.junit.Rule +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import spock.lang.Specification + + +/** + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +class GuiceyRuleTest extends Specification { + + @Rule + GuiceyAppRule RULE = new GuiceyAppRule<>(AutoScanApplication, null); + + def "Test rule usage"() { + + expect: "app initialized" + RULE.getInjector().getExistingBinding(Key.get(Environment)) + RULE.getBean(Environment) + RULE.getConfiguration() + RULE.getEnvironment() + + and: "double init do nothing" + RULE.before() + } +} \ No newline at end of file diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/MultipleRulesTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/MultipleRulesTest.groovy new file mode 100644 index 000000000..a1db3c0a6 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/MultipleRulesTest.groovy @@ -0,0 +1,50 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Injector +import io.dropwizard.core.setup.Environment +import org.junit.Rule +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup +import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 20.04.2015 + */ +class MultipleRulesTest extends Specification { + + @Rule + GuiceyAppRule RULE = new GuiceyAppRule<>(AutoScanApplication, null) + + @Rule + GuiceyAppRule RULE2 = new GuiceyAppRule<>(AutoScanApplication, null) + + @Shared + int initialStates + + void setup() { + // check injectors registered + initialStates = SharedConfigurationState.statesCount() + assert initialStates >= 2 + def inj1 = SharedConfigurationState.lookup(RULE.getApplication(), Injector).get() + def inj2 = SharedConfigurationState.lookup(RULE2.getApplication(), Injector).get() + assert inj1 != inj2 + } + + void cleanupSpec() { + // check injectors correctly unregistered + assert SharedConfigurationState.statesCount() == (initialStates - 2) + } + + def "Check multiple rules"() { + + expect: "guice contexts are different and registered in lookup" + RULE.getBean(Environment) != RULE2.getBean(Environment) + InjectorLookup.getInjector(RULE.getApplication()).isPresent() + InjectorLookup.getInjector(RULE2.getApplication()).isPresent() + + } +} \ No newline at end of file diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorJunitTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorJunitTest.groovy new file mode 100644 index 000000000..7d2f2837f --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorJunitTest.groovy @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test + +import org.junit.Assert +import org.junit.Rule +import org.junit.Test + +/** + * @author Vyacheslav Rusakov + * @since 08.05.2017 + */ +class StartupErrorJunitTest { + + @Rule + public StartupErrorRule rule = StartupErrorRule.create({ out, err -> + Assert.assertTrue(out.contains('sample')) + Assert.assertTrue(err.contains('errrorrrr')) + }) + + @Test + public void checkFail() throws Exception { + System.out.println 'sample' + System.err.println 'errrorrrr' + System.exit(1) + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorRuleTest.groovy b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorRuleTest.groovy new file mode 100644 index 000000000..57b0d0975 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/StartupErrorRuleTest.groovy @@ -0,0 +1,38 @@ +package ru.vyarus.dropwizard.guice.test + +import org.junit.Rule +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 25.03.2017 + */ +class StartupErrorRuleTest extends Specification { + + @Rule + StartupErrorRule rule = StartupErrorRule.create() + + def "Check exit catch"() { + + when: "exiting" + System.out.println 'sample out' + System.err.println 'sample err' + System.exit(1) + + then: "exit intercepted" + thrown(rule.indicatorExceptionType) + rule.output == 'sample out' + rule.error == 'sample err' + } + + def "Check empty output"() { + + when: "exiting" + System.exit(1) + + then: "exit intercepted" + thrown(rule.indicatorExceptionType) + rule.output == '' + rule.error == '' + } +} \ No newline at end of file diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/BaseTest.java b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/BaseTest.java new file mode 100644 index 000000000..30248c17b --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/BaseTest.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.test.hook.unit; + +import com.google.inject.Binder; +import com.google.inject.Module; +import ru.vyarus.dropwizard.guice.test.GuiceyHooksRule; + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +public abstract class BaseTest { + + static GuiceyHooksRule BASE_CONF = new GuiceyHooksRule( + (builder) -> builder.modules(new XMod())); + + + public static class XMod implements Module { + @Override + public void configure(Binder binder) { + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookDwAppTest.java b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookDwAppTest.java new file mode 100644 index 000000000..a1c1df5b0 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookDwAppTest.java @@ -0,0 +1,56 @@ +package ru.vyarus.dropwizard.guice.test.hook.unit; + +import com.google.inject.Binder; +import com.google.inject.Module; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.GuiceyHooksRule; + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +public class ConfigurationHookDwAppTest { + + static DropwizardAppRule RULE = new DropwizardAppRule<>(ConfigurationHookGuiceyAppTest.App.class); + + @ClassRule + public static RuleChain chain = RuleChain + .outerRule(new GuiceyHooksRule((builder) -> builder.modules(new XMod()))) + .around(RULE); + + @Test + public void checkHook() { + final GuiceyConfigurationInfo info = InjectorLookup + .getInstance(RULE.getApplication(), GuiceyConfigurationInfo.class).get(); + Assert.assertTrue(info.getModules().contains(XMod.class)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + public static class XMod implements Module { + @Override + public void configure(Binder binder) { + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookGuiceyAppTest.java b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookGuiceyAppTest.java new file mode 100644 index 000000000..579ace3d0 --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/ConfigurationHookGuiceyAppTest.java @@ -0,0 +1,54 @@ +package ru.vyarus.dropwizard.guice.test.hook.unit; + +import com.google.inject.Binder; +import com.google.inject.Module; +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Bootstrap; +import io.dropwizard.core.setup.Environment; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import ru.vyarus.dropwizard.guice.GuiceBundle; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.GuiceyAppRule; +import ru.vyarus.dropwizard.guice.test.GuiceyHooksRule; + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +public class ConfigurationHookGuiceyAppTest { + + static GuiceyAppRule RULE = new GuiceyAppRule<>(App.class, null); + + @ClassRule + public static RuleChain chain = RuleChain + .outerRule(new GuiceyHooksRule((builder) -> builder.modules(new XMod()))) + .around(RULE); + + @Test + public void checkHook() { + final GuiceyConfigurationInfo info = (GuiceyConfigurationInfo) RULE.getBean(GuiceyConfigurationInfo.class); + Assert.assertTrue(info.getModules().contains(XMod.class)); + } + + public static class App extends Application { + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + public static class XMod implements Module { + @Override + public void configure(Binder binder) { + } + } +} diff --git a/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/MultipleHooksTest.java b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/MultipleHooksTest.java new file mode 100644 index 000000000..eadf3dadb --- /dev/null +++ b/guicey-test-junit4/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/unit/MultipleHooksTest.java @@ -0,0 +1,42 @@ +package ru.vyarus.dropwizard.guice.test.hook.unit; + +import com.google.inject.Binder; +import com.google.inject.Module; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo; +import ru.vyarus.dropwizard.guice.test.GuiceyAppRule; +import ru.vyarus.dropwizard.guice.test.GuiceyHooksRule; + +import java.util.Arrays; + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +public class MultipleHooksTest extends BaseTest { + + static GuiceyAppRule RULE = new GuiceyAppRule<>(ConfigurationHookGuiceyAppTest.App.class, null); + + @ClassRule + public static RuleChain chain = RuleChain + .outerRule(BASE_CONF) + .around(new GuiceyHooksRule((builder) -> builder.modules(new XMod2()))) + .around(RULE); + + + @Test + public void checkHook() { + final GuiceyConfigurationInfo info = (GuiceyConfigurationInfo) RULE.getBean(GuiceyConfigurationInfo.class); + Assert.assertTrue(info.getModules() + .containsAll(Arrays.asList(XMod.class, XMod2.class))); + } + + public static class XMod2 implements Module { + @Override + public void configure(Binder binder) { + } + } +} diff --git a/guicey-test-junit4/src/test/resources/ru/vyarus/dropwizard/guice/config.yml b/guicey-test-junit4/src/test/resources/ru/vyarus/dropwizard/guice/config.yml new file mode 100644 index 000000000..2794bddb4 --- /dev/null +++ b/guicey-test-junit4/src/test/resources/ru/vyarus/dropwizard/guice/config.yml @@ -0,0 +1,3 @@ +foo: 1 +bar: 3 +baa: 4 \ No newline at end of file diff --git a/guicey-test-spock/README.md b/guicey-test-spock/README.md new file mode 100644 index 000000000..7c616a7f0 --- /dev/null +++ b/guicey-test-spock/README.md @@ -0,0 +1,542 @@ +# Spock 1 + +### About + +[Spock 1](http://spockframework.org) test support + +NOTE: Module was extracted from guicey core. Package remains the same to simplify migration (only additional dependency would be required). + +DEPRECATED because implementation relies on deprecated junit 4 rules. Consider [migration to junit 5 (spock 2)](#migration-to-spock-2) + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-test-spock + {guicey.version} + test + +``` + +Gradle: + +```groovy +testImplementation 'ru.vyarus.guicey:guicey-test-spock:{guicey.version}' +``` + +Omit version if guicey BOM used. + +#### With junit 5 + +OR you can use it with junit 5 vintage engine: + +```groovy +testImplementation 'ru.vyarus.guicey:guicey-test-spock:{guicey.version}' +testImplementation 'org.junit.jupiter:junit-jupiter-api' +testRuntimeOnly 'org.junit.jupiter:junit-jupiter' +testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' +``` + +This way you can write both spock (groovy) and junit 5 (java or groovy) tests. + +### Usage + +Provided extensions: + +* `@UseGuiceyApp` - for lightweight tests (without starting web part, only guice context) +* `@UseDropwizardApp` - for complete integration tests + +Both extensions allow using injections directly in specifications (like spock-guice). + +`@UseGuiceyHooks` extension could be used to apply [configuration hook](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/hooks/) +common for all tests. But, it is deprecated in favor of native hooks support in main extensions. + +#### Testing core logic + +`@UseGuiceyApp` runs all guice logic without starting jetty (so resources, servlets and filters will not be available). +`Managed` objects will still be handled correctly. + +```groovy +@UseGuiceyApp(MyApplication) +class AutoScanModeTest extends Specification { + + @Inject MyService service + + def "My service test" { + + when: 'calling service' + def res = service.getSmth() + + then: 'correct result returned' + res == 'hello' + } +``` + +Application started before all tests in annotated class and stopped after them. + +#### Testing web logic + +`@UseDropwizardApp` is useful for complete integration testing (when web part is required): + +```groovy +@UseDropwizardApp(MyApplication) +class WebModuleTest extends Specification { + + @Inject MyService service + + def "Check web bindings"() { + + when: "calling filter" + def res = new URL("http://localhost:8080/dummyFilter").getText() + + then: "filter active" + res == 'Sample filter and service called' + service.isCalled() +``` + +##### Random ports + +In order to start application on random port you can use configuration shortcut: + +```groovy +@UseDropwizardApp(value = MyApplication, randomPorts = true) +``` + +NOTE +Random ports will be applied even if configuration with exact ports provided: +```groovy +@UseDropwizardApp(value = MyApplication, + config = 'path/to/my/config.yml', + randomPorts = true) +``` +Also, random ports support both server types (default and simple) + + +Real ports could be resolved with [ClientSupport](#client) object. + +#### Rest mapping + +Normally, rest mapping configured with `server.rootMapping=/something/*` configuration, but +if you don't use custom configuration class, but still want to re-map rest, shortcut could be used: + +```groovy +@UseDropwizardApp(value = MyApplication, restMapping="something") +``` + +In contrast to config declaration, attribute value may not start with '/' and end with '/*' - +it would be appended automatically. + +This option is only intended to simplify cases when custom configuration file is not yet used in tests +(usually early PoC phase). It allows you to map servlet into application root in test (because rest is no +more resides in root). When used with existing configuration file, this parameter will override file definition. + +#### Guice injections + +Any gucie bean may be injected directly into test field: + +```groovy +@Inject +SomeBean bean +``` + +This may be even bean not declared in guice modules (JIT injection will occur). + +To better understand injection scopes look the following test: + +```groovy +@UseGuiceyApp(AutoScanApplication) +class InjectionTest extends Specification { + + // instance remain the same between tests + @Shared @Inject TestBean sharedBean + + // new instance injected on each test + @Inject TestBean bean + + // the same context used for all tests (in class), so the same bean instance inserted before each test + @Inject TestSingletonBean singletonBean + + def "Check injection types"() { + when: "changing state of injected beans" + sharedBean.value = 10 + bean.value = 5 + singletonBean.value = 15 + + then: "instances are different" + sharedBean.value == 10 + bean.value == 5 + singletonBean.value == 15 + + } + + def "Check shared state"() { + + expect: "shared bean instance is the same, whereas other one re-injected" + sharedBean.value == 10 + bean.value == 0 + singletonBean.value == 15 // the same instance was set before second test + } + + // bean is in prototype scope + static class TestBean { + int value + } + + @Singleton + static class TestSingletonBean { + int value + } +} +``` + +NOTE +Guice AOP will not work on test methods (because test instances not created by guice). + +#### Client + +Both extensions prepare special jersey client instance which could be used for web calls. +It is mostly useful for complete web tests to call rest services and servlets. + +```groovy +@InjectClient +ClientSupport client +``` + +It will also work in static fields or `@Shared` fields. + +Client object provides: + +* Access to [JerseyClient](https://eclipse-ee4j.github.io/jersey.github.io/documentation/2.29.1/client.html) object (for raw calls) +* Shortcuts for querying main, admin or rest contexts (it will count the current configuration automatically) +* Shortcuts for base main, admin or rest contexts base urls (and application ports) + +Example usages: + +```groovy +// GET {rest path}/some +client.targetRest("some").request().buildGet().invoke() + +// GET {main context path}/servlet +client.targetApp("servlet").request().buildGet().invoke() + +// GET {admin context path}/adminServlet +client.targetAdmin("adminServlet").request().buildGet().invoke() +``` + +TIP +All methods above accepts any number of strings which would be automatically combined into correct path: +```groovy +client.targetRest("some", "other/", "/part") +``` +would be correctly combined as "/some/other/part/" + +As you can see test code is abstracted from actual configuration: it may be default or simple server +with any contexts mapping on any ports - target urls will always be correct. + +```groovy +when: "calling rest service" +def res = client.targetRest("some").request().buildGet().invoke() + +then: "response is correct" +res.status == 200 +res.readEntity(String) == "response text" +``` + +Also, if you want to use other client, client object can simply provide required info: + +```groovy +client.getPort() // app port (8080) +client.getAdminPort() // app admin port (8081) +client.basePathApp() // main context path (http://localhost:8080/) +client.basePathAdmin() // admin context path (http://localhost:8081/) +client.basePathRest() // rest context path (http://localhost:8080/) +``` + +Raw client usage: + +```groovy +// call completely external url +client.target("http://somedomain:8080/dummy/").request().buildGet().invoke() +``` + +WARNING +Client object could be injected with both dropwizard and guicey extensions, but in case of guicey extension, +only raw client could be used (because web part not started all other methods will throw NPE) + +#### Configuration + +For both extensions you can configure application with external configuration file: + +```groovy +@UseGuiceyApp(value = MyApplication, + config = 'path/to/my/config.yml' +class ConfigOverrideTest extends Specification { +``` + +Or just declare required values: + +```groovy +@UseGuiceyApp(value = MyApplication, + configOverride = [ + @ConfigOverride(key = "foo", value = "2"), + @ConfigOverride(key = "bar", value = "12") + ]) +class ConfigOverrideTest extends Specification { +``` + +Or use both at once (here overrides will override file values): + +```groovy +@UseGuiceyApp(value = MyApplication, + config = 'path/to/my/config.yml', + configOverride = [ + @ConfigOverride(key = "foo", value = "2"), + @ConfigOverride(key = "bar", value = "12") + ]) +class ConfigOverrideTest extends Specification { +``` + +#### Application test modification + +You can use [hooks to customize application](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/hooks/). + +In both extensions annotation hooks could be declared with attribute: + +```java +@UseDropwizardApp(value = MyApplication, hooks = MyHook) +``` + +or + +```java +@UseGuiceyApp(value = MyApplication, hooks = MyHook) +``` + +Where MyHook is: + +```java +class MyHook implements GuiceyConfigurationHook {} +``` + +##### Hook fields + +Alternatively, you can declare hook directly in test static field: + +```groovy +@EnableHook +static GuiceyConfigurationHook HOOK = { it.modules(new DebugModule()) } +``` + +Any number of fields could be declared. The same way hook could be declared in base test class: + +```groovy +class BaseTest extends Specification { + + // hook in base class + @EnableHook + static GuiceyConfigurationHook BASE_HOOK = { it.modules(new DebugModule()) } +} + +@UseGuiceyApp(value = App, hooks = SomeOtherHook) +class SomeTest extends BaseTest { + + // Another hook + @EnableHook + static GuiceyConfigurationHook HOOK = { it.modules(new DebugModule2()) } +} +``` + +All 3 hooks will work. + +##### Hooks extension + +WARNING +This extension is deprecated in favour of field hooks declarations. + +```groovy +@UseGuiceyHooks(MyBaseHook) +class BaseTest extends Specification { + +} + +@UseGuiceyApp(App) +class SomeTest extends BaseTest {} +``` + +NOTE +You **can still use** test specific hooks together with declared base hook +(to apply some more test-specific configuration). + +WARNING +Only one `@UseGuiceyHooks` declaration may be used in test hierarchy: +for example, you can't declare it in base class and then another one on extended class +- base for a group of tests. This is spock limitation (only one extension will actually work) +but should not be an issue for most cases. + +#### Extension configuration unification + +It is a common need to run multiple tests with the same test application configuration +(same config overrides, same hooks etc.). +Do not configure it in each test, instead move extension configuration into base test class: + +```groovy +@UsetGuiceyApp(...) +abstract class AbstractTest extends Specification { + // here might be helper methods +} +``` + +And now all test classes should simply extend it: + +```groovy +class Test1 extends AbstractTest { + + @Inject + MyService service + + def "Check something"() { ... } +} +``` + +#### Dropwizard startup error + +`StartupErrorRule` may be used to intercept dropwizard `System.exit(1)` call. +But it will work different then for junit: +`then` section is always called with exception (`CheckExitCalled`). +Also, `then` section may be used for assertion after exit calls and so there is +no need to add custom assertion callbacks (required by junit tests). + +```groovy +class ErrorTest extends Specification { + + @Rule StartupErrorRule RULE = StartupErrorRule.create() + + def "Check startup error"() { + + when: "starting app with error" + new MyErrApp().main(['server']) + + then: "startup failed" + thrown(RULE.indicatorExceptionType) + RULE.output.contains('stating application') + RULE.error.contains('some error occur') +``` + +### Spock lifecycle hooks + +```groovy +class MyTest extends Specification { + + @ClassRule @Shared + JunitRule sharedRule = new JunitRule() + + @Rule + JunitRule2 rule = new JunitRule2() + + def setupSpec() { + } + + def setup() { + } + + def "Test method body" () { + setup: + } +} +``` + +NOTE +Class rules are applied once per test class (same as `setupSpec`). +Rules are applied per test method (same as `setup`). + +Setup order: + +* Class rule +* Setup spec method +* Rule +* Setup method +* Test method's setup section + +### Migration to Spock 2 + +There is no special extensions for Spock 2, instead junit 5 integrations +must be used with it, using [special library](https://github.com/xvik/spock-junit5) + +Current spock extensions are almost equivalent to junit5 extensions (in features and behaviour): + +* Instead of `@UseGuiceyApp` use `@TestGuiceyApp` +* Instead of `@UseDropwizardApp` use `@TestDropwizardApp` +* Hooks can be specified with hooks declaration [in extensions](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#application-test-modification) or as [test fields](http://xvik.github.io/dropwizard-guicey/5.4.2/guide/test/junit5/#hook-fields) +* Instead of `StartupErrorRule` use [system-stubs](https://github.com/webcompere/system-stubs) - the successor of system rules + +#### Client + +For `ClientSupport` object, INSTEAD of + +```java +@Inject ClientSupport client +``` + +use parameter injection (possibly in fixture methods too): + +```java +def "Check something"(ClientSupport client) {} +``` + +#### Config overrides + +Junit extension does not require an annotation for each override, so +INSTEAD of: + +```groovy +@UseDropwizardApp(value = App, configOverride = [ + @ConfigOverride(key = "server.rootPath", value = "/rest/*"), + @ConfigOverride(key = "server.applicationContextPath", value = "/prefix"), + @ConfigOverride(key = "server.adminContextPath", value = "/admin") +``` + +Use: + +```groovy +@TestDropwizardApp(value = App, restMapping = "/rest/*", + configOverride = [ + "server.applicationContextPath: /prefix", + "server.adminContextPath: /admin"]) +``` + +Note that `server.rootPath` could be configured with `restMapping` annotation property. + +#### Alternative declaration + +You may also use [alternative declaration](https://xvik.github.io/dropwizard-guicey/5.4.1/guide/test/junit5/#alternative-declaration): + +```groovy +class MyTest extends Specification { + + @RegisterExtension + static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(App) + .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") + .configOverrides("foo: 2", "bar: 12") + .randomPorts() + .hooks(Hook) + .hooks(builder -> builder.disableExtensions(DummyManaged)) + .create() +} +``` + +This is an alternative to previous rules declaration in fields. +It is useful when you need dynamic hook (as lambda) or configuration overrides +require some other extensions. + +Note that config override may be registered with `Supplier`: + +```java +.configOverride("key", () -> { Somewhere.getValue()}) +``` + +!!! warning + Don't use `@Shared` fields instead of static - it wouldn't work! + Also non-static field declaration is not supported by junit extension. diff --git a/guicey-test-spock/build.gradle b/guicey-test-spock/build.gradle new file mode 100644 index 000000000..7cbdefd03 --- /dev/null +++ b/guicey-test-spock/build.gradle @@ -0,0 +1,7 @@ +description = 'Spock 1 test support' + +dependencies { + implementation 'io.dropwizard:dropwizard-testing' + implementation 'org.spockframework:spock-core:1.3-groovy-2.5' + implementation project(':guicey-test-junit4') +} \ No newline at end of file diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ConfigOverride.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ConfigOverride.java new file mode 100644 index 000000000..c672a3ed5 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ConfigOverride.java @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test.spock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Must be used together with {@code @UseGuiceApp} or {@code UseDropwizardApp} to specify configuration overrides. + * + * @see io.dropwizard.testing.ConfigOverride + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigOverride { + + /** + * @return configuration key + */ + String key(); + + /** + * @return configuration key value + */ + String value(); +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/InjectClient.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/InjectClient.java new file mode 100644 index 000000000..50393ebc2 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/InjectClient.java @@ -0,0 +1,25 @@ +package ru.vyarus.dropwizard.guice.test.spock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation for {@link ru.vyarus.dropwizard.guice.test.ClientSupport} spock test field. Extra annotation + * required to remove uncertainty and apply some context (avoid confusion why it works). + * Note that such annotation is not required for junit 5 version because there client may be injected as parameter + * (so this is only a special fix for spock tests to support the same client object). + *

        + * Example usage: {@code @InjectClient ClientSupport client} + *

        + * Must be used on static, shared or regular fields. When used on not + * {@link ru.vyarus.dropwizard.guice.test.ClientSupport} field, error will be thrown. + * + * @author Vyacheslav Rusakov + * @since 26.05.2020 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface InjectClient { +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseDropwizardApp.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseDropwizardApp.java new file mode 100644 index 000000000..5baf880e4 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseDropwizardApp.java @@ -0,0 +1,101 @@ +package ru.vyarus.dropwizard.guice.test.spock; + +import io.dropwizard.core.Application; +import org.spockframework.runtime.extension.ExtensionAnnotation; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.spock.ext.DropwizardAppExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Dropwizard spock extension. Starts dropwizard application before all tests in class and shutdown after them. + *

        + * Gucie injections will work on test class (just annotate required fields with {@link jakarta.inject.Inject}. + * {@link spock.lang.Shared} may be used to define common injection points for all tests in class. + *

        + * Note: {@code setupSpec()} fixture is called after application start and {@code cleanupSpec()} before + * application tear down. + *

        + * Extension would also recognize the following test fields (including super classes): + *

          + *
        • static {@link GuiceyConfigurationHook} annotated with {@link ru.vyarus.dropwizard.guice.test.EnableHook} - hook + * from field will be registered
        • + *
        • {@link ClientSupport} annotated with {@link InjectClient} field will be injected + * with client instance.
        • + *
        + *

        + * Internally based on {@link io.dropwizard.testing.DropwizardTestSupport}. + * + * @author Vyacheslav Rusakov + * @since 03.01.2015 + * @see InjectClient + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtensionAnnotation(DropwizardAppExtension.class) +public @interface UseDropwizardApp { + + /** + * @return application class + */ + Class value(); + + /** + * @return path to configuration file (optional) + */ + String config() default ""; + + /** + * @return list of overridden configuration values (may be used even without real configuration) + */ + ConfigOverride[] configOverride() default {}; + + /** + * Hooks provide access to guice builder allowing complete customization of application context + * in tests. + *

        + * Additional hooks could be declared in static test fields: + * {@code @EnableHook static GuiceyConfigurationHook HOOK = { it.disableExtensions(Something.class)}}. + * + * @return list of hooks to use + * @see GuiceyConfigurationHook for more info + * @see ru.vyarus.dropwizard.guice.test.EnableHook + */ + Class[] hooks() default {}; + + /** + * Enables random ports usage. Supports both simple and default dropwizard servers. Random ports would be + * set even if you specify exact configuration file with configured ports (option overrides configuration). + *

        + * To get port numbers in test use {@link ClientSupport} static field: + *

        {@code @InjectClient ClientSupport client
        +     *
        +     * static setupSpec() {
        +     *     String baseUrl = "http://localhost:" + client.getPort();
        +     *     String baseAdminUrl = "http://localhost:" + client.getAdminPort();
        +     * }
        +     * }
        + * Or use client target methods directly. + * + * @return true to use random ports + */ + boolean randomPorts() default false; + + /** + * Specifies rest mapping path. This is the same as specifying {@link #configOverride()} + * {@code "server.rootMapping=/something/*"}. Specified value would be prefixed with "/" and, if required + * "/*" applied at the end. So it would be correct to specify {@code restMapping = "api"} (actually set value + * would be "/api/*"). + *

        + * This option is only intended to simplify cases when custom configuration file is not yet used in tests + * (usually early PoC phase). It allows you to map servlet into application root in test (because rest is no + * more resides in root). When used with existing configuration file, this parameter will override file definition. + * + * @return rest mapping (empty string - do nothing) + */ + String restMapping() default ""; +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyApp.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyApp.java new file mode 100644 index 000000000..a1e49967d --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyApp.java @@ -0,0 +1,71 @@ +package ru.vyarus.dropwizard.guice.test.spock; + +import io.dropwizard.core.Application; +import org.spockframework.runtime.extension.ExtensionAnnotation; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.spock.ext.GuiceyAppExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Guicey spock extension. Starts guice context only (without web part). {@link io.dropwizard.lifecycle.Managed} + * objects will be still executed correctly. Guice injectior is created before all tests in class and shut down + * after them. + *

        + * Gucie injections will work on test class (just annotate required fields with {@link jakarta.inject.Inject}. + * {@link spock.lang.Shared} may be used to define common injection points for all tests in class. + *

        + * Note: {@code setupSpec()} fixture is called after application start and {@code cleanupSpec()} before + * application tear down. + *

        + * Extension would also recognize the following test fields (including super classes): + *

          + *
        • static {@link GuiceyConfigurationHook} annotated with {@link ru.vyarus.dropwizard.guice.test.EnableHook} - hook + * from field will be registered
        • + *
        • {@link ru.vyarus.dropwizard.guice.test.ClientSupport} annotated with {@link InjectClient} field will be injected + * with client instance. Note that only generic client may be used (to call 3rd party external services), as + * application's web part is not started.
        • + *
        + *

        + * Internally based on {@link io.dropwizard.testing.DropwizardTestSupport}. + * + * @author Vyacheslav Rusakov + * @since 02.01.2015 + * @see InjectClient + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtensionAnnotation(GuiceyAppExtension.class) +public @interface UseGuiceyApp { + + /** + * @return application class + */ + Class value(); + + /** + * @return path to configuration file (optional) + */ + String config() default ""; + + /** + * @return list of overridden configuration values (may be used even without real configuration) + */ + ConfigOverride[] configOverride() default {}; + + /** + * Hooks provide access to guice builder allowing complete customization of application context + * in tests. + *

        + * Additional hooks could be declared in static test fields: + * {@code @EnableHook static GuiceyConfigurationHook HOOK = { it.disableExtensions(Something.class)}}. + * + * @return list of hooks to use + * @see GuiceyConfigurationHook for more info + * @see ru.vyarus.dropwizard.guice.test.EnableHook + */ + Class[] hooks() default {}; +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyHooks.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyHooks.java new file mode 100644 index 000000000..4ee6f79b9 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/UseGuiceyHooks.java @@ -0,0 +1,36 @@ +package ru.vyarus.dropwizard.guice.test.spock; + +import org.spockframework.runtime.extension.ExtensionAnnotation; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.spock.ext.GuiceyConfigurationExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Guicey hook extension. Used to register {@link GuiceyConfigurationHook} in base test class (usually + * with debug extensions, common for all tests). In actual tests use {@code hooks} attribute of + * {@link UseGuiceyApp} or {@link UseDropwizardApp} extensions to apply test-specific configurations. + *

        + * WARNING: only one {@link UseGuiceyHooks} annotation could be used in test hierarchy. For example, + * you can't use it in both base class and test class. This is spock limitation, but should not be an issue for + * most cases. + * + * @author Vyacheslav Rusakov + * @since 12.04.2018 + * @see GuiceyConfigurationHook for more info + * @deprecated additional hooks could be declared in static test fields (even in base test class) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtensionAnnotation(GuiceyConfigurationExtension.class) +@Deprecated +public @interface UseGuiceyHooks { + + /** + * @return list of hooks to use + */ + Class[] value(); +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/AbstractAppExtension.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/AbstractAppExtension.java new file mode 100644 index 000000000..a5c3b99c9 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/AbstractAppExtension.java @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension; +import org.spockframework.runtime.model.SpecInfo; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.util.HooksUtil; + +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Base class for guicey spock extensions. Extensions use {@link DropwizardTestSupport} internally. + * + * @param extension annotation + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +public abstract class AbstractAppExtension extends AbstractAnnotationDrivenExtension { + + private T annotation; + + @Override + public void visitSpecAnnotation(final T useApplication, final SpecInfo spec) { + this.annotation = useApplication; + } + + @Override + public void visitSpec(final SpecInfo spec) { + final Class testType = spec.getReflection(); + final List hooks = SpecialFieldsSupport.findHooks(testType); + hooks.addAll(HooksUtil.create(getHooks(annotation))); + final GuiceyInterceptor interceptor = + new GuiceyInterceptor(spec, buildSupport(annotation, testType), hooks); + final SpecInfo topSpec = spec.getTopSpec(); + topSpec.addSharedInitializerInterceptor(interceptor); + topSpec.addInitializerInterceptor(interceptor); + topSpec.addCleanupSpecInterceptor(interceptor); + } + + /** + * @param annotation extension annotation instance + * @return configuration hooks defined in annotation + */ + protected abstract Class[] getHooks(T annotation); + + /** + * @param annotation extension annotation instance + * @param test test class + * @return environment support object + */ + protected abstract GuiceyInterceptor.EnvironmentSupport buildSupport(T annotation, Class test); + + /** + * Utility method to convert configuration overrides from annotation to rule compatible format. + * + * @param overrides override annotations + * @return dropwizard config override objects + */ + protected ConfigOverride[] convertOverrides( + final ru.vyarus.dropwizard.guice.test.spock.ConfigOverride... overrides) { + final ConfigOverride[] configOverride = new ConfigOverride[overrides.length]; + int i = 0; + for (ru.vyarus.dropwizard.guice.test.spock.ConfigOverride override : overrides) { + configOverride[i++] = ConfigOverride.config(override.key(), override.value()); + } + return configOverride; + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/DropwizardAppExtension.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/DropwizardAppExtension.java new file mode 100644 index 000000000..968e1be76 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/DropwizardAppExtension.java @@ -0,0 +1,58 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import com.google.common.base.Strings; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp; +import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; +import ru.vyarus.dropwizard.guice.test.util.RandomPortsListener; + +/** + * Spock extension for starting complete dropwizard app. + * + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +public class DropwizardAppExtension extends AbstractAppExtension { + + private static final String STAR = "*"; + + @Override + @SuppressWarnings({"unchecked", "PMD.UseDiamondOperator"}) + protected GuiceyInterceptor.EnvironmentSupport buildSupport(final UseDropwizardApp annotation, + final Class test) { + return new GuiceyInterceptor.AbstractEnvironmentSupport(test) { + @Override + protected DropwizardTestSupport build() { + final DropwizardTestSupport support = new DropwizardTestSupport(annotation.value(), + annotation.config(), + buildConfigOverrides(annotation)); + + if (annotation.randomPorts()) { + support.addListener(new RandomPortsListener<>()); + } + + return support; + } + }; + } + + @Override + protected Class[] getHooks(final UseDropwizardApp annotation) { + return annotation.hooks(); + } + + private ConfigOverride[] buildConfigOverrides(final UseDropwizardApp annotation) { + ConfigOverride[] overrides = convertOverrides(annotation.configOverride()); + if (!Strings.isNullOrEmpty(annotation.restMapping())) { + String mapping = PathUtils.leadingSlash(annotation.restMapping()); + if (!mapping.endsWith(STAR)) { + mapping = PathUtils.trailingSlash(mapping) + STAR; + } + overrides = ConfigOverrideUtils.merge(overrides, ConfigOverride.config("server.rootPath", mapping)); + } + return overrides; + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyAppExtension.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyAppExtension.java new file mode 100644 index 000000000..31e38c824 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyAppExtension.java @@ -0,0 +1,70 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.TestCommand; +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp; + +/** + * Spock extension for guice-only lightweight tests. + * + * @author Vyacheslav Rusakov + * @since 02.01.2015 + */ +public class GuiceyAppExtension extends AbstractAppExtension { + + @Override + protected GuiceyInterceptor.EnvironmentSupport buildSupport(final UseGuiceyApp annotation, final Class test) { + return new GuiceyTestEnvironment(annotation, test); + } + + @Override + protected Class[] getHooks(final UseGuiceyApp annotation) { + return annotation.hooks(); + } + + private class GuiceyTestEnvironment extends GuiceyInterceptor.AbstractEnvironmentSupport { + + private final UseGuiceyApp annotation; + private TestCommand command; + + GuiceyTestEnvironment(final UseGuiceyApp annotation, final Class test) { + super(test); + this.annotation = annotation; + } + + @Override + protected DropwizardTestSupport build() { + return create(annotation.value(), + annotation.config(), + convertOverrides(annotation.configOverride())); + } + + @Override + public void after() { + // root call still important to cleanup properties + super.after(); + if (command != null) { + command.stop(); + } + } + + @SuppressWarnings({"unchecked", "checkstyle:Indentation"}) + private DropwizardTestSupport create( + final Class app, + final String configPath, + final ConfigOverride... overrides) { + return new DropwizardTestSupport<>((Class>) app, + configPath, + (String) null, + application -> { + command = new TestCommand<>(application); + return command; + }, + overrides); + } + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationExtension.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationExtension.java new file mode 100644 index 000000000..8038174af --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationExtension.java @@ -0,0 +1,34 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension; +import org.spockframework.runtime.model.SpecInfo; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyHooks; +import ru.vyarus.dropwizard.guice.test.util.HooksUtil; + +/** + * {@link UseGuiceyHooks} extension implementation. + * + * @author Vyacheslav Rusakov + * @since 12.04.2018 + * @deprecated additional hooks may be declared in static test fields + */ +@Deprecated +public class GuiceyConfigurationExtension extends AbstractAnnotationDrivenExtension { + + private Class[] confs; + + @Override + public void visitSpecAnnotation(final UseGuiceyHooks annotation, final SpecInfo spec) { + confs = annotation.value(); + } + + @Override + public void visitSpec(final SpecInfo spec) { + final GuiceyConfigurationHookInterceptor interceptor = + new GuiceyConfigurationHookInterceptor(HooksUtil.create(confs)); + // hook interceptor MUST go first, otherwise guicey will start without these changes + spec.getTopSpec().getSharedInitializerInterceptors().add(0, interceptor); + spec.getTopSpec().addCleanupSpecInterceptor(interceptor); + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationHookInterceptor.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationHookInterceptor.java new file mode 100644 index 000000000..e7236cdee --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyConfigurationHookInterceptor.java @@ -0,0 +1,44 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import org.spockframework.runtime.extension.AbstractMethodInterceptor; +import org.spockframework.runtime.extension.IMethodInvocation; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; + +import java.util.List; + +/** + * Apply hooks according to spock lifecycle. + * + * @author Vyacheslav Rusakov + * @since 12.04.2018 + * @deprecated additional hooks may be declared in static test fields + */ +@Deprecated +public class GuiceyConfigurationHookInterceptor extends AbstractMethodInterceptor { + + private final List hooks; + + /** + * Create hooks interceptor. + * + * @param hooks hooks + */ + public GuiceyConfigurationHookInterceptor(final List hooks) { + this.hooks = hooks; + } + + @Override + public void interceptSharedInitializerMethod(final IMethodInvocation invocation) throws Throwable { + hooks.forEach(GuiceyConfigurationHook::register); + invocation.proceed(); + } + + @Override + public void interceptCleanupSpecMethod(final IMethodInvocation invocation) throws Throwable { + // for case when base test is used without actual application start and so listeners will never be used + // cleanup state after tests to not affect other tests + ConfigurationHooksSupport.reset(); + invocation.proceed(); + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyExtensionException.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyExtensionException.java new file mode 100644 index 000000000..00d75d8bb --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyExtensionException.java @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import org.spockframework.runtime.extension.ExtensionException; + +/** + * Exception thrown in case of exceptional situations in extensions. + * + * @author Vyacheslav Rusakov + * @since 02.01.2015 + */ +public class GuiceyExtensionException extends ExtensionException { + + /** + * Create extension exception. + * + * @param message message + */ + public GuiceyExtensionException(final String message) { + super(message); + } + + /** + * Create extension exception. + * + * @param message message + * @param cause root exception + */ + public GuiceyExtensionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyInterceptor.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyInterceptor.java new file mode 100644 index 000000000..30359dae2 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/GuiceyInterceptor.java @@ -0,0 +1,194 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import com.google.inject.Injector; +import com.google.inject.spi.InjectionPoint; +import io.dropwizard.testing.DropwizardTestSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spockframework.runtime.extension.AbstractMethodInterceptor; +import org.spockframework.runtime.extension.IMethodInvocation; +import org.spockframework.runtime.model.SpecInfo; +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.spock.InjectClient; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.TestFieldUtils; +import spock.lang.Shared; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; + +/** + * Leverages rules logic to start/stop application and injects Guice-provided objects into specifications. + *

        Implementation is very similar to original spock-guice module.

        + * + * @author Vyacheslav Rusakov + * @since 02.01.2015 + */ +// Important implementation detail: Only the fixture methods of +// spec.getTopSpec() are intercepted (see GuiceExtension) +public class GuiceyInterceptor extends AbstractMethodInterceptor { + + private final EnvironmentSupport support; + private final List hooks; + private final Set injectionPoints; + private final List> clientFields; + private Injector injector; + + /** + * Create an interceptor. + * + * @param spec spock spec + * @param support environment support object + * @param hooks hooks + */ + public GuiceyInterceptor(final SpecInfo spec, final EnvironmentSupport support, + final List hooks) { + this.support = support; + this.hooks = hooks; + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(spec.getReflection()); + clientFields = TestFieldUtils + .findAnnotatedFields(spec.getReflection(), InjectClient.class, ClientSupport.class); + } + + @Override + public void interceptSharedInitializerMethod(final IMethodInvocation invocation) throws Throwable { + hooks.forEach(GuiceyConfigurationHook::register); + support.before(); + // static fields + SpecialFieldsSupport.initClients(null, clientFields, support.getClient(), false); + injector = support.getInjector(); + injectValues(invocation.getSharedInstance(), true); + invocation.proceed(); + } + + @Override + public void interceptInitializerMethod(final IMethodInvocation invocation) throws Throwable { + injectValues(invocation.getInstance(), false); + invocation.proceed(); + } + + @Override + public void interceptCleanupSpecMethod(final IMethodInvocation invocation) throws Throwable { + // just in case to avoid side-effects + ConfigurationHooksSupport.reset(); + try { + invocation.proceed(); + } finally { + support.after(); + } + } + + private void injectValues(final Object target, final boolean sharedFields) throws IllegalAccessException { + for (InjectionPoint point : injectionPoints) { + if (!(point.getMember() instanceof Field)) { + throw new GuiceyExtensionException("Method injection is not supported; use field injection instead"); + } + + final Field field = (Field) point.getMember(); + if (field.isAnnotationPresent(Shared.class) != sharedFields) { + continue; + } + + final Object value = injector.getInstance(point.getDependencies().get(0).getKey()); + field.setAccessible(true); + field.set(target, value); + } + // ClientSupport fields + SpecialFieldsSupport.initClients(target, clientFields, support.getClient(), sharedFields); + } + + /** + * External junit rules adapter. + */ + public interface EnvironmentSupport { + + /** + * Prepare environment. + * + * @throws Exception on error + */ + void before() throws Exception; + + /** + * Shutdown environment. + * + * @throws Exception on error + */ + void after() throws Exception; + + /** + * @return injector instance + */ + Injector getInjector(); + + /** + * @return client object + */ + ClientSupport getClient(); + } + + /** + * Base environment support implementation. Used as-is for dropwizard test and requires advanced command + * handling for guicey test (because dropwizard support will not properly shutdown it). + */ + public abstract static class AbstractEnvironmentSupport implements EnvironmentSupport { + private final Logger logger = LoggerFactory.getLogger(AbstractEnvironmentSupport.class); + + @SuppressWarnings("PMD.UnusedPrivateField") + private final Class test; + private DropwizardTestSupport support; + private ClientSupport client; + + /** + * Create environment support. + * + * @param test test class + */ + public AbstractEnvironmentSupport(final Class test) { + this.test = test; + } + + /** + * @return created support object instance + */ + protected abstract DropwizardTestSupport build(); + + @Override + public void before() throws Exception { + support = build(); + support.before(); + + client = new ClientSupport(support); + } + + @Override + public void after() { + if (support != null) { + support.after(); + } + if (client != null) { + try { + client.close(); + } catch (Exception e) { + // not critical, just info + logger.info("Error closing client instance", e); + } + } + } + + @Override + public Injector getInjector() { + return InjectorLookup.getInjector(support.getApplication()) + .orElseThrow(() -> new IllegalStateException("No active injector found")); + } + + @Override + public ClientSupport getClient() { + return client; + } + } +} diff --git a/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/SpecialFieldsSupport.java b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/SpecialFieldsSupport.java new file mode 100644 index 000000000..a792595e2 --- /dev/null +++ b/guicey-test-spock/src/main/java/ru/vyarus/dropwizard/guice/test/spock/ext/SpecialFieldsSupport.java @@ -0,0 +1,68 @@ +package ru.vyarus.dropwizard.guice.test.spock.ext; + +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.EnableHook; +import ru.vyarus.dropwizard.guice.test.spock.InjectClient; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.AnnotatedField; +import ru.vyarus.dropwizard.guice.test.jupiter.env.field.TestFieldUtils; +import spock.lang.Shared; + +import java.util.ArrayList; +import java.util.List; + +/** + * Support for special test fields. Injects {@link ClientSupport} and accepts + * {@link GuiceyConfigurationHook} fields. + * + * @author Vyacheslav Rusakov + * @since 17.05.2020 + */ +public final class SpecialFieldsSupport { + + private SpecialFieldsSupport() { + } + + /** + * Search guicey hooks in static test fields (including super classes). + * + * @param test test class + * @return list of found hook objects or empty list + */ + public static List findHooks(final Class test) { + final List hooks = new ArrayList<>(); + final List> fields = TestFieldUtils.findAnnotatedFields( + test, EnableHook.class, GuiceyConfigurationHook.class); + fields.forEach(AnnotatedField::requireStatic); + for (GuiceyConfigurationHook hook : TestFieldUtils.getValues(fields, null)) { + if (hook != null) { + hooks.add(hook); + } + } + return hooks; + } + + /** + * Injects client object into static test fields (including super class). + * + * @param instance test instance (null for static injection) + * @param fields all client fields (static, shared, instance) + * @param client client instance + * @param shared process shared fields + */ + public static void initClients(final Object instance, + final List> fields, + final ClientSupport client, + final boolean shared) { + fields.forEach(field -> { + // skip instance injections for static and static injection for instance + final boolean incompatibleField = (instance == null && !field.isStatic()) + || (instance != null && field.isStatic()); + if (field.getField().isAnnotationPresent(Shared.class) != shared || incompatibleField) { + return; + } + field.setValue(instance, client); + }); + } +} + diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy new file mode 100644 index 000000000..ebdeb6c97 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/AutoScanApplication.groovy @@ -0,0 +1,35 @@ +package ru.vyarus.dropwizard.guice.support + +import io.dropwizard.core.Application +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.support.util.BindModule + +/** + * Example of automatic configuration: installers, beans and commands searched automatically. + * @author Vyacheslav Rusakov + * @since 01.09.2014 + */ +class AutoScanApplication extends Application { + + public static void main(String[] args) { + new AutoScanApplication().run(args) + } + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .enableAutoConfig("ru.vyarus.dropwizard.guice.support.feature") + .searchCommands() + .modules(new BindModule(Service)) + .build() + ); + } + + @Override + void run(TestConfiguration configuration, Environment environment) throws Exception { + } + + static class Service {} +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java new file mode 100644 index 000000000..e460d86d5 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/TestConfiguration.java @@ -0,0 +1,22 @@ +package ru.vyarus.dropwizard.guice.support; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.core.Configuration; + +/** + * Groovy class can't be used anymore, because jackson 2.5 is very sensible for additional methods. + * + * @author Vyacheslav Rusakov + * @since 01.09.2014 + */ +public class TestConfiguration extends Configuration { + + @JsonProperty + public int foo; + + @JsonProperty + public int bar; + + @JsonProperty + public int baa; +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy new file mode 100644 index 000000000..0a87ec808 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyResource.groovy @@ -0,0 +1,26 @@ +package ru.vyarus.dropwizard.guice.support.feature + +import com.google.inject.Inject + +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 01.09.2014 + */ +@Path("/dummy") +@Produces('application/json') +class DummyResource { + + @Inject + DummyService service + + @GET + @Path("/") + public Response latest() { + return Response.ok().build(); + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy new file mode 100644 index 000000000..16d1c310f --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/feature/DummyService.groovy @@ -0,0 +1,15 @@ +package ru.vyarus.dropwizard.guice.support.feature + +import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton + +/** + * @author Vyacheslav Rusakov + * @since 03.09.2014 + */ +@EagerSingleton +class DummyService { + + public String hey() { + 'hey!' + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy new file mode 100644 index 000000000..cb90433a0 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/support/util/BindModule.groovy @@ -0,0 +1,24 @@ +package ru.vyarus.dropwizard.guice.support.util + +import com.google.inject.AbstractModule + +/** + * Module used to explicitly bind services normally resolved by JIT. + * + * @author Vyacheslav Rusakov + * @since 19.06.2016 + */ +class BindModule extends AbstractModule { + private Class[] types + + BindModule(Class... types) { + this.types = types + } + + @Override + protected void configure() { + types.each { + bind(it) + } + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/CustomRestMappingTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/CustomRestMappingTest.groovy new file mode 100644 index 000000000..b2381e309 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/CustomRestMappingTest.groovy @@ -0,0 +1,28 @@ +package ru.vyarus.dropwizard.guice.test + + +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp +import spock.lang.Specification + +import jakarta.ws.rs.client.ClientBuilder +import jakarta.ws.rs.core.Response + +/** + * @author Vyacheslav Rusakov + * @since 20.05.2020 + */ +@UseDropwizardApp(value = AutoScanApplication.class, restMapping = "api") +class CustomRestMappingTest extends Specification { + + def "Check custom rest prefix"() { + Response response = ClientBuilder.newClient() + .target("http://localhost:8080/api/dummy/") + .request() + .buildGet() + .invoke(); + + expect: "prefix applied" + response.status == 200 + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/DWConfigOverrideTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/DWConfigOverrideTest.groovy new file mode 100644 index 000000000..22976d24a --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/DWConfigOverrideTest.groovy @@ -0,0 +1,32 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Inject +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import ru.vyarus.dropwizard.guice.test.spock.ConfigOverride +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +@UseDropwizardApp(value = AutoScanApplication, + config = 'src/test/resources/ru/vyarus/dropwizard/guice/config.yml', + configOverride = [ + @ConfigOverride(key = "foo", value = "2"), + @ConfigOverride(key = "bar", value = "12") + ]) +class DWConfigOverrideTest extends Specification { + + @Inject + TestConfiguration configuration + + def "Check config override"() { + + expect: "config overridden" + configuration.foo == 2 + configuration.bar == 12 + configuration.baa == 4 + } +} \ No newline at end of file diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/EmptyConfigOverrideTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/EmptyConfigOverrideTest.groovy new file mode 100644 index 000000000..ad902858b --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/EmptyConfigOverrideTest.groovy @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Inject +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import ru.vyarus.dropwizard.guice.test.spock.ConfigOverride +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import spock.lang.Specification + + +/** + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +@UseGuiceyApp(value = AutoScanApplication, + // NOTE: no config file specified + configOverride = [ + @ConfigOverride(key = "foo", value = "2"), + @ConfigOverride(key = "bar", value = "12") + ]) +class EmptyConfigOverrideTest extends Specification { + + @Inject + TestConfiguration configuration + + def "Check config override"() { + + expect: "config overridden (filled)" + configuration.foo == 2 + configuration.bar == 12 + configuration.baa == 0 + } +} \ No newline at end of file diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyConfigOverrideTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyConfigOverrideTest.groovy new file mode 100644 index 000000000..b6a832655 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/GuiceyConfigOverrideTest.groovy @@ -0,0 +1,33 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Inject +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.TestConfiguration +import ru.vyarus.dropwizard.guice.test.spock.ConfigOverride +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import spock.lang.Specification + + +/** + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +@UseGuiceyApp(value = AutoScanApplication, + config = 'src/test/resources/ru/vyarus/dropwizard/guice/config.yml', + configOverride = [ + @ConfigOverride(key = "foo", value = "2"), + @ConfigOverride(key = "bar", value = "12") + ]) +class GuiceyConfigOverrideTest extends Specification { + + @Inject + TestConfiguration configuration + + def "Check config override"() { + + expect: "config overridden" + configuration.foo == 2 + configuration.bar == 12 + configuration.baa == 4 + } +} \ No newline at end of file diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy new file mode 100644 index 000000000..678176a8d --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/InjectionTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.test + +import com.google.inject.Inject +import com.google.inject.Singleton +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import spock.lang.Shared +import spock.lang.Specification + + +/** + * @author Vyacheslav Rusakov + * @since 03.01.2015 + */ +@UseGuiceyApp(AutoScanApplication) +class InjectionTest extends Specification { + + // instance remain the same between tests + @Shared @Inject TestBean sharedBean + + // new instance injected on each test + @Inject TestBean bean + + // the same context used for all tests (in class), so the same bean instance inserted before each test + @Inject TestSingletonBean singletonBean + + def "Check injection types"() { + when: "changing state of injected beans" + sharedBean.value = 10 + bean.value = 5 + singletonBean.value = 15 + + then: "instances are different" + sharedBean.value == 10 + bean.value == 5 + singletonBean.value == 15 + + } + + def "Check shared state"() { + + expect: "shared bean instance is the same, whereas other one re-injected" + sharedBean.value == 10 + bean.value == 0 + singletonBean.value == 15 // the same instance was set before second test + } + + // bean is in prototype scope + static class TestBean { + int value + } + + @Singleton + static class TestSingletonBean { + int value + } +} \ No newline at end of file diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy new file mode 100644 index 000000000..29abe2f5c --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/LifecycleStartedForGuiceyTest.groovy @@ -0,0 +1,45 @@ +package ru.vyarus.dropwizard.guice.test + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import org.eclipse.jetty.util.component.LifeCycle +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 31.07.2016 + */ +@UseGuiceyApp(App) +class LifecycleStartedForGuiceyTest extends Specification { + + static boolean called + + def "Check lifecycle started for lightweight guicey test"() { + + expect: + called + + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + environment.lifecycle().addEventListener(new LifeCycle.Listener() { + @Override + void lifeCycleStarted(LifeCycle event) { + called = true + } + }) + } + } +} \ No newline at end of file diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/RandomPortsTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/RandomPortsTest.groovy new file mode 100644 index 000000000..a4ec98d10 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/RandomPortsTest.groovy @@ -0,0 +1,31 @@ +package ru.vyarus.dropwizard.guice.test + +import io.dropwizard.core.Configuration +import io.dropwizard.jetty.HttpConnectorFactory +import io.dropwizard.core.server.DefaultServerFactory +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 17.05.2020 + */ +@UseDropwizardApp(value = AutoScanApplication.class, randomPorts = true) +class RandomPortsTest extends Specification { + + @Inject + Configuration config + + def "Check random ports"() { + + setup: + DefaultServerFactory factory = (DefaultServerFactory) config.getServerFactory() + + expect: "random ports applied" + ((HttpConnectorFactory) factory.getApplicationConnectors().first()).getPort() == 0 + ((HttpConnectorFactory) factory.getAdminConnectors().first()).getPort() == 0 + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/SpecialFieldsTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/SpecialFieldsTest.groovy new file mode 100644 index 000000000..1cb873635 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/SpecialFieldsTest.groovy @@ -0,0 +1,53 @@ +package ru.vyarus.dropwizard.guice.test + +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.support.AutoScanApplication +import ru.vyarus.dropwizard.guice.support.feature.DummyResource +import ru.vyarus.dropwizard.guice.test.spock.InjectClient +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp +import spock.lang.Shared +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 21.05.2020 + */ +@UseDropwizardApp(AutoScanApplication) +class SpecialFieldsTest extends Specification { + + @EnableHook + static GuiceyConfigurationHook hook = { builder -> + builder.disableExtensions(DummyResource) + } as GuiceyConfigurationHook + + @InjectClient + static ClientSupport client + + @InjectClient + @Shared + ClientSupport clientShared + + @InjectClient + ClientSupport clientInstance + + @Inject + GuiceyConfigurationInfo info + + def "Check static fields support"() { + + expect: "hook applied" + info.getExtensionsDisabled().contains(DummyResource) + + and: "static client injected" + client != null + client.getPort() == 8080 + client.getAdminPort() == 8081 + + and: "non static injections" + clientShared == client + clientInstance == client + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy new file mode 100644 index 000000000..7acdbee8a --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/ConfigurationHooksSupportTest.groovy @@ -0,0 +1,50 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +class ConfigurationHooksSupportTest extends Specification { + + void cleanupSpec() { + // to avoid side effects for other tests + ConfigurationHooksSupport.reset() + } + + def "Check hook registration lifecycle"() { + + when: "register hook" + ({} as GuiceyConfigurationHook).register() + then: "ok" + ConfigurationHooksSupport.count() == 1 + + when: "check processing" + ConfigurationHooksSupport.run(GuiceBundle.builder(), new StatsTracker()) + then: "hooks flushed" + ConfigurationHooksSupport.count() == 0 + + when: "check reset call" + ({} as GuiceyConfigurationHook).register() + ConfigurationHooksSupport.reset() + then: "ok" + ConfigurationHooksSupport.count() == 0 + } + + def "Check listener execution"() { + + def init = false + + when: + ({ init = true } as GuiceyConfigurationHook).register() + ConfigurationHooksSupport.run(GuiceBundle.builder(), new StatsTracker()) + then: "called" + init + + } +} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy similarity index 78% rename from src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy rename to guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy index 78b7a6b89..06663f4cd 100644 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DiagnosticHookEnableTest.groovy @@ -1,19 +1,19 @@ package ru.vyarus.dropwizard.guice.test.hook -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment import ru.vyarus.dropwizard.guice.GuiceBundle import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport -import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp import spock.lang.Specification /** * @author Vyacheslav Rusakov * @since 18.08.2019 */ -@TestGuiceyApp(App) +@UseGuiceyApp(App) class DiagnosticHookEnableTest extends Specification { void cleanup() { diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DwAppHookAttributeTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DwAppHookAttributeTest.groovy new file mode 100644 index 000000000..f0efcba40 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/DwAppHookAttributeTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Binder +import com.google.inject.Module +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.spock.UseDropwizardApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +@UseDropwizardApp(value = App, hooks = Hook) +class DwAppHookAttributeTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + + def "Check hook attribute works"() { + + expect: "module registered" + info.getModules().contains(XMod) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Hook implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new XMod()) + } + } + + static class XMod implements Module { + @Override + void configure(Binder binder) { + + } + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceyAppHookAttributeTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceyAppHookAttributeTest.groovy new file mode 100644 index 000000000..50b3acebc --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/GuiceyAppHookAttributeTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Binder +import com.google.inject.Module +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +@UseGuiceyApp(value = App, hooks = Hook) +class GuiceyAppHookAttributeTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + + def "Check hook attribute works"() { + + expect: "module registered" + info.getModules().contains(XMod) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Hook implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new XMod()) + } + } + + static class XMod implements Module { + @Override + void configure(Binder binder) { + + } + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy new file mode 100644 index 000000000..57ed935db --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/SystemHooksTest.groovy @@ -0,0 +1,65 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import spock.lang.Specification + +/** + * @author Vyacheslav Rusakov + * @since 18.08.2019 + */ +class SystemHooksTest extends Specification { + + void setup() { + ConfigurationHooksSupport.reset() + } + + void cleanup() { + ConfigurationHooksSupport.reset() + } + + def "Check system hooks loading"() { + + when: "declare and load hook" + System.setProperty(ConfigurationHooksSupport.HOOKS_PROPERTY, Hook.name) + ConfigurationHooksSupport.loadSystemHooks() + then: "hook loaded" + ConfigurationHooksSupport.count() == 1 + + } + + def "Check alias registration"() { + + when: "register alias" + ConfigurationHooksSupport.registerSystemHookAlias("hook", Hook) + then: "alias registered" + ConfigurationHooksSupport.getSystemHookAliases()["hook"] == Hook.name + + when: "load hook by alias" + System.setProperty(ConfigurationHooksSupport.HOOKS_PROPERTY, "hook") + ConfigurationHooksSupport.loadSystemHooks() + then: "loaded" + ConfigurationHooksSupport.count() == 1 + + when: "declare duplicate alias" + ConfigurationHooksSupport.registerSystemHookAlias("hook", HookOverride) + then: "ok, just warning printed" + true + + } + + static class Hook implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + + } + } + + static class HookOverride implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + + } + } +} diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtAndAttribute.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtAndAttribute.groovy new file mode 100644 index 000000000..b46c90c87 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtAndAttribute.groovy @@ -0,0 +1,75 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Binder +import com.google.inject.Module +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyHooks +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +@UseGuiceyApp(value = App, hooks = Hook) +@UseGuiceyHooks(Hook2) +class UseHookExtAndAttribute extends Specification { + + @Inject + GuiceyConfigurationInfo info + + def "Check hook attribute works"() { + + expect: "module registered" + info.getModules().containsAll(XMod, XMod2) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Hook implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new XMod()) + } + } + + static class XMod implements Module { + @Override + void configure(Binder binder) { + + } + } + + + static class Hook2 implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new XMod2()) + } + } + + static class XMod2 implements Module { + @Override + void configure(Binder binder) { + + } + } +} + diff --git a/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtTest.groovy b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtTest.groovy new file mode 100644 index 000000000..897843653 --- /dev/null +++ b/guicey-test-spock/src/test/groovy/ru/vyarus/dropwizard/guice/test/hook/UseHookExtTest.groovy @@ -0,0 +1,59 @@ +package ru.vyarus.dropwizard.guice.test.hook + +import com.google.inject.Binder +import com.google.inject.Module +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook +import ru.vyarus.dropwizard.guice.module.GuiceyConfigurationInfo +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyApp +import ru.vyarus.dropwizard.guice.test.spock.UseGuiceyHooks +import spock.lang.Specification + +import jakarta.inject.Inject + +/** + * @author Vyacheslav Rusakov + * @since 13.04.2018 + */ +@UseGuiceyApp(App) +@UseGuiceyHooks(Hook) +class UseHookExtTest extends Specification { + + @Inject + GuiceyConfigurationInfo info + + def "Check hook extension works"() { + + expect: "module registered" + info.getModules().contains(XMod) + } + + static class App extends Application { + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder().build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Hook implements GuiceyConfigurationHook { + @Override + void configure(GuiceBundle.Builder builder) { + builder.modules(new XMod()) + } + } + + static class XMod implements Module { + @Override + void configure(Binder binder) { + + } + } +} diff --git a/guicey-test-spock/src/test/resources/ru/vyarus/dropwizard/guice/config.yml b/guicey-test-spock/src/test/resources/ru/vyarus/dropwizard/guice/config.yml new file mode 100644 index 000000000..2794bddb4 --- /dev/null +++ b/guicey-test-spock/src/test/resources/ru/vyarus/dropwizard/guice/config.yml @@ -0,0 +1,3 @@ +foo: 1 +bar: 3 +baa: 4 \ No newline at end of file diff --git a/guicey-validation/README.md b/guicey-validation/README.md new file mode 100644 index 000000000..9a7a0d37c --- /dev/null +++ b/guicey-validation/README.md @@ -0,0 +1,118 @@ +# Validation + +### About + +By default, dropwizard allows you to use validation annotations on [rest services](https://www.dropwizard.io/en/stable/manual/validation.html). +This allows you to use validation annotations the same way on any guice bean method. + +Bundle is actually a wrapper for [guice-validator](https://github.com/xvik/guice-validator) project. + +### Setup + +Maven: + +```xml + + ru.vyarus.guicey + guicey-validation + {guicey.version} + +``` + +Gradle: + +```groovy +implementation 'ru.vyarus.guicey:guicey-validation:{guicey.version}' +``` + +Omit version if guicey BOM used. + +### Usage + +By default, no setup required: bundle will be loaded automatically with the bundles lookup mechanism (enabled by default). +So just add jar into classpath and annotations will work. + +For example: + +```java +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import ru.vyarus.guicey.annotations.lifecycle.PostStartup; + +public class SampleBean { + + private void doSomething(@NotNull String param) { + } + +} +``` + +Call `bean.doSomething(null)` will fail with `ConstraintValidationException`. + +For more usage examples see [guice-validator documentation](https://github.com/xvik/guice-validator#examples) + +#### Explicit mode + +By default, validations work in implicit mode: any method containing validation annotations would trigger validation +on call. + +If you want more explicitly mark methods requiring validation then register bundle manually: + +```java +.bundles(new ValidationBundle() + .validateAnnotatedOnly()) +``` + +Now, only methods annotated with `@ValidateOnExecution` (or all methods in annotated class) +will trigger validation. + +If you want, you can use your own annotation: + +```java +.bundles(new ValidationBundle() + .validateAnnotatedOnly(MyAnnotation.class)) +``` + +#### Reducing scope + +By default, validation is not applied to resource classes (annotated with `@Path`) because +dropwizard already performs validation there. And rest methods, annotated with `@GET`, `@POST`, etc. +are skipped (required for complex declaration cases, like dynamic resource mappings or sub resources). + +You can reduce this scope even further: + +```java +.bundles(new ValidationBundle() + .targetClasses(Matchers.subclassesOf(SomeService.class) + .and(Matchers.not(Matchers.annotatedWith(Path.class))))) +``` + +Here `SomeService` is excluded from validation (its methods would not trigger validation). +Note that default condition (not resource) is appended. + + +Or excluding methods: + +```java +.bundles(new ValidationBundle() + .targetMethods(Matchers.annotatedWith(SuppressValidation.class) + .and(new DirectMethodMatcher()))) +``` + +Now methods annotated with `@SuppressValidation` will not be validated. Note that +`.and(new DirectMethodMatcher())` condition was added to aslo exclude synthetic and bridge methods (jvm generated methods). + +NOTE: you can verify AOP appliance with guicey `.printGuiceAopMap()` report. + +#### Validation groups + +By default, `Default` validation group is always enabled allowing you to not specify +groups for each call. + +This could be disabled with bundle option: + +```java +.bundles(new ValidationBundle().strictGroupsDeclaration()) +``` + +Read more in [guice-validator docs](https://github.com/xvik/guice-validator#default-group-specifics). \ No newline at end of file diff --git a/guicey-validation/build.gradle b/guicey-validation/build.gradle new file mode 100644 index 000000000..5a2cb412b --- /dev/null +++ b/guicey-validation/build.gradle @@ -0,0 +1,8 @@ +description = "Guice-managed pre/post validations for bean methods" + +dependencies { + implementation ('ru.vyarus:guice-validator') { + exclude group: 'com.google.inject', module: 'guice' + exclude group: 'jakarta.validation', module: 'validation-api' + } +} \ No newline at end of file diff --git a/guicey-validation/src/main/java/ru/vyarus/guicey/validation/ValidationBundle.java b/guicey-validation/src/main/java/ru/vyarus/guicey/validation/ValidationBundle.java new file mode 100644 index 000000000..d342945e6 --- /dev/null +++ b/guicey-validation/src/main/java/ru/vyarus/guicey/validation/ValidationBundle.java @@ -0,0 +1,168 @@ +package ru.vyarus.guicey.validation; + +import com.google.inject.matcher.Matcher; +import com.google.inject.matcher.Matchers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.vyarus.dropwizard.guice.module.context.unique.item.UniqueGuiceyBundle; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap; +import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment; +import ru.vyarus.guice.validator.ValidationModule; +import ru.vyarus.guice.validator.aop.DeclaredMethodMatcher; +import ru.vyarus.guicey.validation.util.RestMethodMatcher; + +import jakarta.validation.Validator; +import jakarta.validation.executable.ValidateOnExecution; +import jakarta.ws.rs.Path; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * Validation bundle activates implicit method validations for guice beans. It means that if method have + * any jakarta.validation annotations (contraints for parameters ot return value) then validations would be + * performed. By default, dropwizard applies validation only to rest resources, this bundle activates validations + * for all guice beans. + *

        + * Bundle registered automatically by bundles lookup. But if you want to configure it, simply register it directly + * (and lookup-provided instance would be ignored). + *

        + * See {@link ValidationModule} for more info. Bundle essentially just provide shortcuts for module configurations. + *

        + * Bundle also binds {@link Validator} and {@link jakarta.validation.executable.ExecutableValidator}, so they become + * available for injection. Custom validators may use guice injections. + *

        + * WARNING: do not obtain validator directly from {@link jakarta.validation.ValidatorFactory} because it will not + * be able to wire guice injections for validators requiring it. Module substitute {@link Validator} instance in + * dropwizard {@link io.dropwizard.core.setup.Environment} so custom guice-aware validators may be used on rest + * resources too, + * + * @author Vyacheslav Rusakov + * @since 26.12.2019 + */ +public class ValidationBundle extends UniqueGuiceyBundle { + private final Logger logger = LoggerFactory.getLogger(ValidationBundle.class); + + // most resources annotated directly + private Matcher typeMatcher = Matchers.not(Matchers.annotatedWith(Path.class)); + + // in complex declaration cases, avoid methods with @GET, @POST, etc. annotations + private Matcher methodMatcher = new DeclaredMethodMatcher() + .and(Matchers.not(new RestMethodMatcher())); + private Class targetAnnotation; + private boolean strictGroups; + + + /** + * Customize target classes to apply validation on. By default, it would be all classes not annotated + * with {@link Path}. + *

        + * If you declare your own target matcher, make sure it also avoids rest services: + * {@code yourMatcher.and(Matchers.not(Matchers.annotatedWith(Path.class)))}. + *

        + * Shortcut for {@link ValidationModule#targetClasses(Matcher)}. + * + * @param matcher matcher + * @return bundle instance + */ + public ValidationBundle targetClasses(final Matcher matcher) { + typeMatcher = matcher; + return this; + } + + /** + * Customize target methods to apply validation on. By default, all methods except annotated with rest + * annotations ({@link jakarta.ws.rs.GET}, link {@link jakarta.ws.rs.POST}, etc.) are allowed (see + * {@link RestMethodMatcher}. Also, synthetic methods avoided. + *

        + * It is better to also exclude synthetic and bridge methods from matching: you can simply add direct method + * matcher: {@code yourMatcher.and(new DirectMethodMatcher())}. + *

        + * Shortcut for {@link ValidationModule#targetMethods(Matcher)}. + *

        + * Note: it is possible to "implement" explicit mode with this matcher (like + * {@code Matchers.annotatedWith(MyAnn.class)}), but better use {@link #validateAnnotatedOnly(Class)}. + * Method call will produce correct log and eventually will extend your matcher with annotation condition. + * But, if you want to implement exclusion annotation, then method matcher is the best choice: + * {@code Matchers.not(Matchers.annotatedWith(SuppressValidation.class))} will lead to validation + * suppression on all annotated methods. + * + * @param matcher matcher + * @return bundle instance + */ + public ValidationBundle targetMethods(final Matcher matcher) { + methodMatcher = matcher; + return this; + } + + /** + * Activates explicit mode, when only {@link ValidateOnExecution} annotated methods (or all methods in + * annotated class) are validated. + *

        + * Shortcut for {@link ValidationModule#validateAnnotatedOnly()}. + * + * @return bundle instance + */ + public ValidationBundle validateAnnotatedOnly() { + return validateAnnotatedOnly(ValidateOnExecution.class); + } + + /** + * Same as {@link #validateAnnotatedOnly()}, but you can specify custom annotation. + *

        + * Shortcut for {@link ValidationModule#validateAnnotatedOnly(Class)}. + * + * @param annotation annotation to trigger validation + * @return bundle instance + */ + public ValidationBundle validateAnnotatedOnly(final Class annotation) { + this.targetAnnotation = annotation; + return this; + } + + /** + * By default, ({@link jakarta.validation.groups.Default}) group is always added to groups + * defined with {@link ru.vyarus.guice.validator.group.annotation.ValidationGroups} annotation. + *

        + * Calling this method disables default behavior: after calling it, {@link jakarta.validation.groups.Default} + * must be explicitly declared. + * + * @return bundle instance + */ + public ValidationBundle strictGroupsDeclaration() { + this.strictGroups = true; + return this; + } + + @Override + public void initialize(final GuiceyBootstrap bootstrap) { + // excluding rest beans because dropwizard already applies validation support there + final ValidationModule module = new ValidationModule(bootstrap.bootstrap().getValidatorFactory()) + .targetClasses(typeMatcher) + .targetMethods(methodMatcher); + + if (targetAnnotation != null) { + module.validateAnnotatedOnly(targetAnnotation); + } + + if (strictGroups) { + module.strictGroupsDeclaration(); + } + + bootstrap.modules(module); + } + + @Override + public void run(final GuiceyEnvironment environment) throws Exception { + // substitute dropwizard validator with guice-aware validator in order to be able + // to use custom (guice-aware) validators in resources + environment.onGuiceyStartup((config, env, injector) -> { + env.setValidator(injector.getInstance(Validator.class)); + if (targetAnnotation == null) { + logger.info("Validation annotations support enabled on guice beans"); + } else { + logger.info("Validation annotations support enabled on guice beans and methods, " + + "annotated with @{}", targetAnnotation.getSimpleName()); + } + }); + } +} diff --git a/guicey-validation/src/main/java/ru/vyarus/guicey/validation/util/RestMethodMatcher.java b/guicey-validation/src/main/java/ru/vyarus/guicey/validation/util/RestMethodMatcher.java new file mode 100644 index 000000000..1ae18113f --- /dev/null +++ b/guicey-validation/src/main/java/ru/vyarus/guicey/validation/util/RestMethodMatcher.java @@ -0,0 +1,33 @@ +package ru.vyarus.guicey.validation.util; + +import com.google.inject.matcher.Matcher; +import jakarta.ws.rs.HttpMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * Matcher denies methods annotated with jax-rs annotations. This is required to avoid + * duplicate validations (because dropwizard already applies validations on rest). + *

        + * Normally, all rest resources are annotated with {@link jakarta.ws.rs.Path} so it is easy to filter all rest classes. + * This matcher is required only for complex declaration cases. + * + * @author Vyacheslav Rusakov + * @since 26.12.2019 + */ +public class RestMethodMatcher implements Matcher { + + @Override + public boolean matches(final Method method) { + boolean res = false; + for (Annotation ann : method.getDeclaredAnnotations()) { + // found @GET, @POST, etc. annotated method + if (ann.annotationType().getDeclaredAnnotation(HttpMethod.class) != null) { + res = true; + break; + } + } + return res; + } +} diff --git a/guicey-validation/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle b/guicey-validation/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle new file mode 100644 index 000000000..8beeb7666 --- /dev/null +++ b/guicey-validation/src/main/resources/META-INF/services/ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle @@ -0,0 +1 @@ +ru.vyarus.guicey.validation.ValidationBundle \ No newline at end of file diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ExplicitValidationTest.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ExplicitValidationTest.groovy new file mode 100644 index 000000000..63829afa0 --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ExplicitValidationTest.groovy @@ -0,0 +1,65 @@ +package ru.vyarus.guicey.validation + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject +import jakarta.validation.ConstraintViolationException +import jakarta.validation.constraints.NotNull +import jakarta.validation.executable.ValidateOnExecution + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@TestGuiceyApp(App) +class ExplicitValidationTest extends Specification { + + @Inject + Service service + + def "Check implicit validation enabled"() { + + when: "call method with incorrect parameter" + service.call(null) + then: "no validation because no explicit activator" + true + + when: "call annotated method with incorrect parameter" + service.call2(null) + then: "validation applied" + thrown(ConstraintViolationException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new ValidationBundle() + .validateAnnotatedOnly()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + public void call(@NotNull Object arg) { + + } + + @ValidateOnExecution + public void call2(@NotNull Object arg) { + + } + } +} diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ImplicitValidationTest.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ImplicitValidationTest.groovy new file mode 100644 index 000000000..7aeb18ca1 --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ImplicitValidationTest.groovy @@ -0,0 +1,57 @@ +package ru.vyarus.guicey.validation + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject +import jakarta.validation.ConstraintViolationException +import jakarta.validation.constraints.NotNull + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@TestGuiceyApp(App) +class ImplicitValidationTest extends Specification { + + @Inject + Service service + + def "Check implicit validation enabled"() { + + when: "call service with incorrect parameter" + service.call(null) + then: "validation failed" + thrown(ConstraintViolationException) + + when: "call with correct param" + service.call(12) + then: "ok" + true + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + public void call(@NotNull Object arg) { + + } + } +} diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/RestMethodMatcherTest.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/RestMethodMatcherTest.groovy new file mode 100644 index 000000000..fae7eb452 --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/RestMethodMatcherTest.groovy @@ -0,0 +1,43 @@ +package ru.vyarus.guicey.validation + +import ru.vyarus.guicey.validation.util.RestMethodMatcher +import spock.lang.Specification + +import jakarta.validation.executable.ValidateOnExecution +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +class RestMethodMatcherTest extends Specification { + + def "Check matcher"() { + + def matcher = new RestMethodMatcher() + expect: "correct methods recognition" + matcher.matches(Service.getMethod(name)) == res + + where: + name | res + 'method' | false + 'getMethod' | true + 'postMethod' | true + 'otherAnnotation' | false + } + + static class Service { + + public void method() {} + + @GET + public void getMethod() {} + + @POST + public void postMethod() {} + + @ValidateOnExecution + public void otherAnnotation() {} + } +} diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ScopeCustomizationTest.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ScopeCustomizationTest.groovy new file mode 100644 index 000000000..fbbb54eb5 --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/ScopeCustomizationTest.groovy @@ -0,0 +1,91 @@ +package ru.vyarus.guicey.validation + +import com.google.inject.matcher.Matchers +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import spock.lang.Specification + +import jakarta.inject.Inject +import jakarta.validation.ConstraintViolationException +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull + +/** + * @author Vyacheslav Rusakov + * @since 30.12.2019 + */ +@TestGuiceyApp(App) +class ScopeCustomizationTest extends Specification { + + @Inject + Service service + + @Inject + Service2 service2 + + def "Check implicit validation enabled"() { + + when: "service 1 ignored" + service.call(null) + service.call2(null) + then: "no validation because no explicit activator" + true + + when: "service 2 simple method ignored" + service2.call(null) + then: "no validation" + true + + when: "service 2 annotated method checked" + service2.call2(null) + then: "validation applied" + thrown(ConstraintViolationException) + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new ValidationBundle() + // avoid service 1 + .targetClasses(Matchers.not(Matchers.subclassesOf(Service.class))) + // validate only methods with @Valid + // NOTE explicit mode is not enabled! its pure scope manipulation + .targetMethods(Matchers.annotatedWith(Valid.class))) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } + + static class Service { + + public void call(@NotNull Object arg) { + + } + + @Valid + public Object call2(@NotNull Object arg) { + + } + } + + static class Service2 { + + public void call(@NotNull Object arg) { + + } + + @Valid + public Object call2(@NotNull Object arg) { + + } + } +} diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/Group1.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/Group1.groovy new file mode 100644 index 000000000..785eab9f2 --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/Group1.groovy @@ -0,0 +1,21 @@ +package ru.vyarus.guicey.validation.group + +import ru.vyarus.guice.validator.group.annotation.ValidationGroups + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +import static java.lang.annotation.ElementType.METHOD +import static java.lang.annotation.ElementType.TYPE + +/** + * @author Vyacheslav Rusakov + * @since 07.09.2021 + */ +@Target([TYPE, METHOD]) +@Retention(RetentionPolicy.RUNTIME) +@ValidationGroups(Group1) +@interface Group1 { + +} diff --git a/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/NoDefaultGroupTest.groovy b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/NoDefaultGroupTest.groovy new file mode 100644 index 000000000..9d515909c --- /dev/null +++ b/guicey-validation/src/test/groovy/ru/vyarus/guicey/validation/group/NoDefaultGroupTest.groovy @@ -0,0 +1,74 @@ +package ru.vyarus.guicey.validation.group + +import io.dropwizard.core.Application +import io.dropwizard.core.Configuration +import io.dropwizard.core.setup.Bootstrap +import io.dropwizard.core.setup.Environment +import ru.vyarus.dropwizard.guice.GuiceBundle +import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp +import ru.vyarus.guice.validator.group.ValidationContext +import ru.vyarus.guicey.validation.ValidationBundle +import spock.lang.Specification + +import jakarta.inject.Inject +import jakarta.validation.ConstraintViolationException +import jakarta.validation.Valid +import jakarta.validation.constraints.NotNull +import jakarta.validation.groups.Default + +/** + * @author Vyacheslav Rusakov + * @since 07.09.2021 + */ +@TestGuiceyApp(App) +class NoDefaultGroupTest extends Specification { + + @Inject + Service service + @Inject + ValidationContext context + + def "Check default groups not used"() { + + when: "valid model used" + service.call(new Model(foo: "sample", bar: null)) + then: "ok" + true + + when: "call with default group" + context.doWithGroups({ + service.call(new Model(foo: "sample", bar: null)) + }, Default) + then: "error" + thrown(ConstraintViolationException) + } + + static class Model { + + @NotNull(groups = Group1) + String foo + @NotNull + String bar + } + + static class Service { + + @Group1 + void call(@Valid Model model) { + } + } + + static class App extends Application { + + @Override + void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(GuiceBundle.builder() + .bundles(new ValidationBundle().strictGroupsDeclaration()) + .build()) + } + + @Override + void run(Configuration configuration, Environment environment) throws Exception { + } + } +} diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 000000000..46c852919 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fd354975c..3767814f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,16 +4,17 @@ pluginManagement { gradlePluginPortal() } } -plugins { - id "com.gradle.enterprise" version "3.10.2" -} -gradleEnterprise { - buildScan { - termsOfServiceUrl = 'https://gradle.com/terms-of-service' - termsOfServiceAgree = 'yes' - //publishOnFailure() - } -} +// have to use different name, otherwise gradle lose its mind +rootProject.name = 'dropwizard-guicey-parent' -rootProject.name = 'dropwizard-guicey' +include 'dropwizard-guicey', + 'guicey-eventbus', + 'guicey-jdbi3', + 'guicey-spa', + 'guicey-lifecycle-annotations', + 'guicey-server-pages', + 'guicey-admin-rest', + 'guicey-validation', + 'guicey-test-spock', + 'guicey-test-junit4' diff --git a/src/doc/docs/about/release-notes.md b/src/doc/docs/about/release-notes.md deleted file mode 100644 index f52298b75..000000000 --- a/src/doc/docs/about/release-notes.md +++ /dev/null @@ -1,319 +0,0 @@ -# 5.6.0 Release Notes - -!!! summary "" - [5.5.0 release notes](http://xvik.github.io/dropwizard-guicey/5.5.0/about/release-notes/) - -* Dropwizard 2.1 compatibility -* Junit 5 extensions enhancements - -## Dropwizard 2.1 compatibility - -Release upgrades guicey to [dropwizard 2.1](https://github.com/dropwizard/dropwizard/releases/tag/v2.1.0) - -[Dropwizard upgrade notes](https://www.dropwizard.io/en/latest/manual/upgrade-notes/upgrade-notes-2_1_x.html#upgrade-notes-for-dropwizard-2-1-x) - -### Java 8 issue - -!!! warning - On java 8 you'll see the following warning on application startup: - - ``` - WARN [2022-06-06 16:39:24,946] com.fasterxml.jackson.module.blackbird.BlackbirdModule: Unable to find Java 9+ MethodHandles.privateLookupIn. Blackbird is not performing optimally! - ! java.lang.NoSuchMethodError: java.lang.invoke.MethodHandles.privateLookupIn(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup; - ! at java.lang.invoke.MethodHandleNatives.resolve(Native Method) - ! at java.lang.invoke.MemberName$Factory.resolve(MemberName.java:975) - ! at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1000) - ! ... 86 common frames omitted - ! Causing: java.lang.NoSuchMethodException: no such method: java.lang.invoke.MethodHandles.privateLookupIn(Class,Lookup)Lookup/invokeStatic - ! at java.lang.invoke.MemberName.makeAccessException(MemberName.java:871) - ! at java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1003) - ! at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:1386) - ! at java.lang.invoke.MethodHandles$Lookup.findStatic(MethodHandles.java:780) - ! at com.fasterxml.jackson.module.blackbird.util.ReflectionHack$Java9Up.init(ReflectionHack.java:39) - ! at com.fasterxml.jackson.module.blackbird.util.ReflectionHack$Java9Up.(ReflectionHack.java:34) - ... - ``` - -It looks like an exception, but this is **just a warning**! Application work will not be affected. - -This happens because dropwizard replaced afterburner with blackbird. -Change appeared in [io.dropwizard.jackson.Jackson](https://github.com/dropwizard/dropwizard/blob/release/2.1.x/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java#L58): - -before afterburner was registered ONLY for java 8: - -```java -mapper.registerModule(new JodaModule()); -if (JavaVersion.isJava8()) { - mapper.registerModule(new AfterburnerModule()); -} -mapper.registerModule(new FuzzyEnumModule()); -``` - -now blackbird is always registered: - -```java -mapper.registerModule(new JodaModule()); -mapper.registerModule(new BlackbirdModule()); -mapper.registerModule(new FuzzyEnumModule()); -``` - -!!! note - Both modules are not required: they just enhance jackson performance in some (high-pressure) scenarios. - Afterburner supposed to be used for java 8 (and works well!). Blackbird is a rewrite of afterburner for - java 11 (and above) compatibility, because afterburner has problems on recent java versions. - Blackbird has java 8 **compatibility**, but it was never meant to be used for java 8! - -The obvious solution would be to disable blackbird for java 8: there was [a discussion proposing this](https://github.com/dropwizard/dropwizard/discussions/5319), -but dropwizard maintainers did not wish to do it (I still hope they would change their mind). - -You can't hide this message [with logger configuration](https://github.com/dropwizard/dropwizard/discussions/5268#discussioncomment-2723607) -because it happens before logger initialization. - -The only way to hide it is to manually construct application `ObjectMapper`: - -```java -@Override -public void initialize(Bootstrap bootstrap) { - ObjectMapper mapper = new ObjectMapper() - mapper.registerModule(new GuavaModule()); - mapper.registerModule(new GuavaExtrasModule()); - mapper.registerModule(new CaffeineModule()); - mapper.registerModule(new JodaModule()); - - // optional but preferred - mapper.registerModule(new AfterburnerModule()); - - mapper.registerModule(new FuzzyEnumModule()); - mapper.registerModule(new ParameterNamesModule()); - mapper.registerModule(new Jdk8Module()); - mapper.registerModule(new JavaTimeModule()); - mapper.setPropertyNamingStrategy(new AnnotationSensitivePropertyNamingStrategy()); - mapper.setSubtypeResolver(new DiscoverableSubtypeResolver()); - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - // override default mapper - bootstrap.setObjectMapper(mapper); -``` - -As you can see this is basically a copy of dropwizard [Jackson class](https://github.com/dropwizard/dropwizard/blob/release/2.1.x/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java#L58) -logic with one change. - -To use afterburner you'll need an additional dependency: - -```groovy -implementation 'com.fasterxml.jackson.module:jackson-module-afterburner:2.13.0' -``` - -## Junit 5 extensions enhancements - -### Test support objects - -#### Environment setup - -In order to simplify environment setup in tests, new interface added: - -```java -public interface TestEnvironmentSetup { - Object setup(TestExtension extension); -} -``` - -For example, it might be used to setup test database: - -```java -public class TestDbSetup implements TestEnvironmentSetup { - - @Override - public Object setup(TestExtension extension) { - // pseudo code - Db db = DbFactory.startTestDb(); - // register required configuration - extension - .configOverride("database.url", ()-> db.getUrl()) - .configOverride("database.user", ()-> db.getUser()) - .configOverride("database.password", ()-> db.getPassword); - // assuming object implements Closable - return db; - } -} -``` - -!!! tip "Motivation" - Previously, additional junit extensions were required for such kind of setup, - but there was a problem with configuration (because guicey generates system property - key for each test and so it is not possible to configure application directly with - system property. - Also, it was problematic to move such initialization into base class because - it could be done only with static fields. - New interface should greatly simplify maintaining test environments. - -Only configuration overrides and guicey hooks are allowed for registration. - -!!! note - To avoid confusion with guicey hooks: setup object required to prepare test environment before test (and apply - required configurations) whereas hooks is a general mechanism for application customization (not only in tests). - Setup objects are executed before application startup (before `DropwizardTestSupport` object creation) and hooks - are executed by started application. - -It is often required not only to start/create something before test, but also -properly stop/destroy it after. To do it simply return any `Closable` (or `AutoClosable`) -and it would be called just after application shutdown. - -If no managed object required - you may return whatever else (even null), nothing would happen. -This was done to simplify lambda declarations. - -##### Registration - -Registration is the same as with hooks: - -* `setup` attribute in extension annotations -* `setup()` methods in extension builders (registered in fields) -* Test fields, annotated with `@EnableSetup` - -Simple lambdas might be used for registration, for example: - -```java -@EnableSetup -static TestEnvironmentSetup db = ext -> { - Db db = new Db(); - ext.configOverride("db.url", ()->db.getUrl()) - return db; - }; -``` - -Field-based declaration might be useful when such initializations must be declared -in base test class (and affect all tests). - -#### Test init log - -All registered setup objects and hooks (registered in test) now logged on test startup: - -``` -18:39:03.390 [main] INFO r.v.d.g.t.j.e.c.TestExtensionsTracker - Guicey test extensions: - - Setup objects = - HookObjectsLogTest$Test2$$Lambda$349/1644231115 (r.v.d.g.t.j.hook) @EnableSetup field Test2.setup - - Test hooks = - HookObjectsLogTest$Base$$Lambda$341/1127224355 (r.v.d.g.t.j.hook) @EnableHook field Base.base1 - Ext1 (r.v.d.g.t.j.h.HookObjectsLogTest) @RegisterExtension class - HookObjectsLogTest$Test2$$Lambda$345/484589713 (r.v.d.g.t.j.hook) @RegisterExtension instance - Ext3 (r.v.d.g.t.j.h.HookObjectsLogTest) HookObjectsLogTest$Test2$$Lambda$349/1644231115 class - HookObjectsLogTest$Test2$$Lambda$369/1911152052 (r.v.d.g.t.j.hook) HookObjectsLogTest$Test2$$Lambda$349/1644231115 instance - HookObjectsLogTest$Test2$$Lambda$350/537066525 (r.v.d.g.t.j.hook) @EnableHook field Test2.ext1 -``` - -Setup objects and hooks are shown in execution order. Setup objects go first because they -might also register hooks. Registration source hints are shown on the right. -There should be enough information to clearly understand test initialization sequence. - -#### @EnableHook field type - -Before, it was impossible to use exact hook class type in field declaration: - -```java -@EnableHook -static GuiceyConfigurationHook hook = new MyHook(); -``` - -But now any class could be used: - -```java -@EnableHook -static MyHook hook = new MyHook(); -``` - -### Extensions registration changes - -#### Start application for each test method - -It is now possible to start application before each test method: - -```java -@RegisterExtension -TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() - -// injection would be re-newed for each test method -@Inject Bean bean; - -@Test -public void test1() { - Assertions.assertEquals(0, bean.value); - bean.value = 10 -} - -@Test -public void test2() { - Assertions.assertEquals(0, bean.value); - bean.value = 10 -} -``` - -Note that field is **not static**. In this case extension would be activated for each method. - -Also, `@EnableHook` and `@EnableSetup` fields might also be not static (but static fields would also work) in this case: - -```java -@RegisterExtension -TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() - -@EnableSetup -MySetup setup = new MySetup() -``` - -#### Configure application from 3rd party junit extension - -!!! note - Generally, setup objects usage should be simpler then writing additional junit - extensions for environment setup, but if you already have an extension, - the following should simplify configuration. - -3rd party junit extension should only store required values using junit storage and -they could be applied now with a new method `configOverrideByExtension`: - -```java -public class ConfigExtension implements BeforeAllCallback { - - @Override - public void beforeAll(ExtensionContext context) throws Exception { - // do something and then store value - context.getStore(ExtensionContext.Namespace.GLOBAL).put("ext1", 10); - } -} - -@ExtendWith(ConfigExtension.class) -public class SampleTest { - - @RegisterExtension - static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) - .configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1") - .create(); -} -``` - -Here junit extension stores value and guicey extension will retrieve and apply value -from store. Configuration path and storage key are the same here, but they could be different: - - -```java -.configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "storage.key", "config.path") -``` - -Apply configuration path 'config.path' value from junit storage under key `storage.key`. - - -#### Small builder improvements - -`.hooks(Class)` method now accepts multiple hooks at once: - -```java -.hooks(Hook1.class, Hook2.class); -``` - -`.configOverrides(String...)` method now could be called multiple times: - -```java -.configOverrides("foo:1", "bar:2") -.configOverrides("over:3") -``` - diff --git a/src/doc/docs/about/support.md b/src/doc/docs/about/support.md deleted file mode 100644 index 5181d2bc6..000000000 --- a/src/doc/docs/about/support.md +++ /dev/null @@ -1,5 +0,0 @@ -# Support - -* [Discussions](https://github.com/xvik/dropwizard-guicey/discussions) - most likely will replace google group -* [Gitter](https://gitter.im/xvik/dropwizard-guicey) - chat -* [Github issues](https://github.com/xvik/dropwizard-guicey/issues) - problems / enhancements diff --git a/src/doc/docs/examples/jdbi.md b/src/doc/docs/examples/jdbi.md deleted file mode 100644 index cc564b650..000000000 --- a/src/doc/docs/examples/jdbi.md +++ /dev/null @@ -1,259 +0,0 @@ -# JDBI integration - -Example of [guicey-jdbi](../extras/jdbi.md) extension usage. - -!!! abstract "" - Example [source code](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-jdbi) - - -The [JDBI extension](../extras/jdbi.md) allows: - -* using jdbi proxies as guice beans -* using injection inside proxies -* using AOP on proxies -* using annotations for transaction definition -* automatic repository and mapper installation - -## Configuration - -Additional dependencies required: - -```groovy -implementation 'ru.vyarus.guicey:guicey-jdbi:{{ gradle.ext }}' -implementation 'com.h2database:h2:1.4.199' -``` - -!!! note - guicey-jdbi version could be managed with [BOM](../extras/bom.md) - -[dropwizard-jdbi](https://www.dropwizard.io/en/release-1.3.x/manual/jdbi.html) is used to configure -and create dbi instance: - -```java -public class JdbiAppConfiguration extends Configuration { - - @Valid - @NotNull - @JsonProperty - private DataSourceFactory database = new DataSourceFactory(); - - public DataSourceFactory getDatabase() { - return database; - } -} -``` - -For simplicity, an embedded H2 database is used: - -```yaml -database: - driverClass: org.h2.Driver - user: sa - password: - url: jdbc:h2:~/sample - properties: - charSet: UTF-8 - maxWaitForConnection: 1s - validationQuery: "SELECT 1" - validationQueryTimeout: 3s - minSize: 8 - maxSize: 32 - checkConnectionWhileIdle: false - evictionInterval: 10s - minIdleTime: 1 minute -``` - -!!! warning - Database scheme must be created manually. You can use - [dropwizard-flyway](https://github.com/dropwizard/dropwizard-flyway) module to prepare database. - See [example app source](https://github.com/xvik/dropwizard-guicey-examples/tree/master/ext-jdbi) for details. - - -DBI instance created exactly as described in [dropwizard docs](https://www.dropwizard.io/en/release-1.3.x/manual/jdbi.html) -using provided db configuration: - -```java -GuiceBundle.builder() - .bundles(JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())) -``` - -!!! note - You can use [pre-build dbi instance](../extras/jdbi.md#usage) instead. - -## Repository definition - -!!! warning - All jdbi repositories must be annotated with `@JdbiRepository` to let the [repository installer](../extras/jdbi.md#repository) - recognize and properly install them. - -```java -@JdbiRepository -@InTransaction -public abstract class UserRepository extends Crud { - - // have to use field injection because class is still used by dbi (which is no aware of guice) for proxy creation - @Inject - private RandomNameGenerator generator; - - // sample of hybrid method in repository, using injected service - public User createRandomUser() { - final User user = new User(); - user.setName(generator.generateName()); - save(user); - return user; - } - - @Override - @SqlUpdate("insert into users (name, version) values (:name, :version)") - @GetGeneratedKeys - public abstract long insert(@UserBind User entry); - - @SqlUpdate("update users set version=:version, name=:name where id=:id and version=:version - 1") - @Override - public abstract int update(@UserBind User entry); - - @SqlQuery("select * from users") - public abstract List findAll(); - - @SqlQuery("select * from users where name = :name") - public abstract User findByName(@Bind("name") String name); -} -``` - -Where `Crud` base class tries to unify repositories and provide hibernate-like optimistic locking behaviour -(on each entity save version field is assigned/incremented and checked during update to prevent data loss): - -```java -public abstract class Crud { - - @InTransaction - public T save(final T entry) { - // hibernate-like optimistic locking mechanism: provided entity must have the same version as in database - if (entry.getId() == 0) { - entry.setVersion(1); - entry.setId(insert(entry)); - } else { - final int ver = entry.getVersion(); - entry.setVersion(ver + 1); - if (update(entry) == 0) { - throw new ConcurrentModificationException(String.format( - "Concurrent modification for object %s %s version %s", - entry.getClass().getName(), entry.getId(), ver)); - } - } - return entry; - } - - public abstract long insert(T entry); - - public abstract int update(T entry); -} -``` - -!!! note "" - You don't necessarily need to use `Crud` - it's an advanced usage example. - -The repository is annotated with `@InTransaction` to allow direct usage; repository method calls are the smallest transaction scope. -The transaction scope can be enlarged by using annotations on calling guice beans or -[declaring transactions manually](../extras/jdbi.md#manual-transaction-definition). -In order to better understand how transactions work, read the [unit of work docs section](../extras/jdbi.md#unit-of-work). - -!!! note - `@InTransaction` is handled with guice AOP, so you can use any other guice aop related features. - -!!! attention - Constructor injection is impossible in repositories, but you can use field injections: - ```java - @Inject - private RandomNameGenerator generator; - ``` - -## Result set mapper - -Result set mapper is used to map query result set to entity: - -```java -public class UserMapper implements ResultSetMapper { - - @Override - public User map(int index, ResultSet r, StatementContext ctx) throws SQLException { - User user = new User(); - user.setId(r.getLong("id")); - user.setVersion(r.getInt("version")); - user.setName(r.getString("name")); - return user; - } -} -``` - -Mappers are installed with the [mapper installer](../extras/jdbi.md#result-set-mapper). -If auto scan is enabled then all mappers will be detected automatically and registered in the dbi instance. -Mappers are instantiated as normal guice beans without restrictions which means you can use injection and aop -(it's only not shown in example mapper). - -!!! note - The mapper installer mostly automates (and unifies) registration. If your mapper does not need to be guice bean - and you dont want to use auto configuration then you can register it manually in dbi instance, making it available for injection. - -Also, see complementing binding annotation, used to bind object to query parameters: - -```java -@BindingAnnotation(UserBind.UserBinder.class) -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface UserBind { - - class UserBinder implements BinderFactory { - @Override - public Binder build(UserBind annotation) { - return (Binder) (q, bind, arg) -> { - q.bind("id", arg.getId()) - .bind("version", arg.getVersion()) - .bind("name", arg.getName()); - }; - } - } -} -``` - -See `@UserBind` usage above in repository definition. - -There is no custom installer for annotation because it's detected automatically by DBI. - -## Usage - -Repositories are used as normal guice beans: - -```java -@Path("/users") -@Produces("application/json") -public class UserResource { - - @Inject - private final UserRepository repository; - - @POST - @Path("/") - public User create(String name) { - User user = new User(); - user.setName(name); - return repository.save(user); - } - - @GET - @Path("/") - public List findAll() { - return repository.findAll(); - } -} -``` - -`UserMapper` and `UserBind` are used implicitly to convert the POJO into a db record and back. - -You can use `@InTransaction` on repository method to enlarge transaction scope, but, in contrast -to hibernate you dont't have to always declare it to avoid lazy initialization exception -(because jdbi produces simple pojos). - -!!! note - `@InTrasaction` is named to avoid confusion with the commonly used `@Transactional` annotation. - You [can bind any annotation class](../extras/jdbi.md#intransaction) if you like to use a different name (the annotation is just a marker) diff --git a/src/doc/docs/extras/jdbi.md b/src/doc/docs/extras/jdbi.md deleted file mode 100644 index 624988f5e..000000000 --- a/src/doc/docs/extras/jdbi.md +++ /dev/null @@ -1,268 +0,0 @@ -# JDBI integration - -!!! summary "" - [Extensions project](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi) module - -!!! warning "" - **DEPRECATED**: because jdbi2 dropwizard module is deprecated and moved [outside of core modules](https://github.com/dropwizard/dropwizard-jdbi). - Migrate [to jdbi3](#migration-to-jdbi3) - -Integrates [JDBI2](http://jdbi.org/) with guice. Based on [dropwizard-jdbi](https://www.dropwizard.io/en/release-1.3.x/manual/jdbi.html) integration. - -Features: - -* DBI instance available for injection -* Introduce unit of work concept, which is managed by annotations and guice aop (very like spring's @Transactional) -* Repositories (JDBI proxies for interfaces and abstract classes): - - installed automatically (when classpath scan enabled) - - are normal guice beans, supporting aop and participating in global (thread bound) transaction. - - no need to compose repositories anymore (e.g. with @CreateSqlObject) to gain single transaction. -* Automatic installation for custom `ResultSetMapper` - -Added installers: - -* [RepositoryInstaller](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi/src/main/java/ru/vyarus/guicey/jdbi/installer/repository/RepositoryInstaller.java) - sql proxies -* [MapperInstaller](https://github.com/xvik/dropwizard-guicey-ext/tree/master/guicey-jdbi/src/main/java/ru/vyarus/guicey/jdbi/installer/MapperInstaller.java) - result set mappers - -## Setup - -[![Maven Central](https://img.shields.io/maven-central/v/ru.vyarus.guicey/guicey-jdbi.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.vyarus.guicey/guicey-jdbi) - -Avoid version in dependency declaration below if you use [extensions BOM](bom.md). - -Maven: - -```xml - - ru.vyarus.guicey - guicey-jdbi - {{ gradle.ext }} - -``` - -Gradle: - -```groovy -implementation 'ru.vyarus.guicey:guicey-jdbi:{{ gradle.ext }}' -``` - -See the most recent version in the badge above. - -## Usage - -Register bundle: - -```java -GuiceBundle.builder() - .bundles(JdbiBundle.forDatabase((conf, env) -> conf.getDatabase())) - ... -``` - -Here default DBI instance will be created from database configuration (much like it's described in -[dropwizard documentation](https://www.dropwizard.io/en/release-1.3.x/manual/jdbi.html)). - -Or build DBI instance yourself: - -```java -JdbiBundle.forDbi((conf, env) -> locateDbi()) -``` - -### Unit of work - -Unit of work concept states for: every database related operation must be performed inside unit of work. - -In DBI such approach was implicit: you were always tied to initial handle. This lead to cumbersome usage of -sql object proxies: if you create it on-demand it would always create new handle; if you want to combine -multiple objects in one transaction, you have to always create them manually for each transaction. - -Integration removes these restrictions: dao (repository) objects are normal guice beans and transaction -scope is controlled by `@InTransaction` annotation (note that such name was intentional to avoid confusion with -DBI own's Transaction annotation and more common Transactional annotations). - -At the beginning of unit of work, DBI handle is created and bound to thread (thread local). -All repositories are simply using this bound handle and so share transaction inside unit of work. - -#### @InTransaction - -Annotation on method or class declares transactional scope. For example: - -```java -@Inject MyDAO dao - -@InTransaction -public Result doSomething() { - dao.select(); - ... -} -``` - -Transaction opened before doSomething() method and closed after it. -Dao call is also performed inside transaction. -If exception appears during execution, it's propagated and transaction rolled back. - -Nested annotations are allowed (they simply ignored). - -Note that unit of work is not the same as transaction scope (transaction scope could be less or equal to unit of work). -But, for simplicity, you may think of it as the same things, if you always use `@InTransaction` annotation. - -If required, you may use your own annotation for transaction definition: - -```java -JdbiBundle.forDatabase((conf, env) -> conf.getDatabase()) - .withTxAnnotations(MyCustomTransactional.class); -``` - -Note that this will override default annotation support. If you want to support multiple annotations then specify -all of them: - -```java -JdbiBundle.forDatabase((conf, env) -> conf.getDatabase()) - .withTxAnnotations(InTransaction.class, MyCustomTransactional.class); -``` - -#### Context Handle - -Inside unit of work you may reference current handle by using: - -```java -@Inject Provider -``` - -#### Manual transaction definition - -You may define transaction (with unit of work) without annotation using: - -```java -@Inject TransactionTenpate template; -... -template.inTrabsansaction((handle) -> doSomething()) -``` - -Note that inside such manual scope you may also call any repository bean, as it's absolutely the same definition as -with annotation. - -### Repository - -Declare repository (interface or abstract class) as usual, using DBI annotations. -It only must be annotated with `@JdbiRepository` so installer -could recognize it and register in guice context. - -!!! warning "" - Singleton scope will be forced for repositories. - -```java -@JdbiRepository -@InTransaction -public interface MyRepository { - - @SqlQuery("select name from something where id = :id") - String findNameById(@Bind("id") int id); -} -``` - -Note the use of `@InTransaction`: it was used to be able to call repository methods without extra annotations -(the lowest transaction scope it's repository itself). It will make beans "feel the same" as usual DBI on demand -sql object proxies. - -`@InTransaction` annotation is handled using guice aop. You can use any other guice aop related features. - -You can also use injection inside repositories, but only field injection: - -```java -public abstract class MyRepo { - @Inject SomeBean bean; -} -``` - -Constructor injection is impossible, because DBI sql proxies are still used internally and DBI will not be able -to construct proxy for class with constructor injection. - -*Don't use DBI @Transaction and @CreateSqlObject annotations anymore*: probably they will even work, but they are not -needed now and may confuse. - -All installed repositories are reported into console: - -``` -INFO [2016-12-05 19:42:27,374] ru.vyarus.guicey.jdbi.installer.repository.RepositoryInstaller: repositories = - - (ru.vyarus.guicey.jdbi.support.repository.SampleRepository) -``` - -### Result set mapper - -If you have custom implementations of `ResultSetMapper`, it may be registered automatically. -You will be able to use injections there because mappers become ususal guice beans (singletons). -When classpath scan is enabled, such classes will be searched and installed automatically. - -```java -public class CustomMapper implements ResutlSetMapper { - @Override - public Cusom map(int row, ResultSet rs, StatementContext ctx) { - // mapping here - return custom; - } -} -``` - -And now Custom type could be used for queries: - -```java -@JdbiRepository -@InTransaction -public interface CustomRepository { - - @SqlQuery("select * from custom where id = :id") - Custom findNameById(@Bind("id") int id); -} -``` - -All installed mappers are reported to console: - -``` -INFO [2016-12-05 20:02:25,399] ru.vyarus.guicey.jdbi.installer.MapperInstaller: jdbi mappers = - - Sample (ru.vyarus.guicey.jdbi.support.mapper.SampleMapper) -``` - -## Manual unit of work definition - -If, for some reason, you don't need transaction at some place, you can declare raw unit of work and use -assigned handle directly: - -```java -@Inject UnitManager manager; - -manager.beginUnit(); -try { - Handle handle = manager.get(); - // logic executed in unit of work but without transaction -} finally { - manager.endUnit(); -} -``` - -Repositories could also be called inside such manual unit (as unit of work is correctly started). - -## Migration to jdbi3 - -* Use [guicey-jdbi3](jdbi3.md) - -* Module package changed from `ru.vyarus.guicey.jdbi` to `ru.vyarus.guicey.jdbi3`. - -* `Jdbi` object was previously bind as `DBI` interface. Now it's bound as `Jdbi` (as interface was removed in jdbi3). - -* New methods in JdbiBundle: - - withPlugins - install custom plugins - - withConfig - to simplify manual configuration - -* In jdbi3 `ResultSetMapper` was changed to `RowMapper` (and ColumnMapper). Installer supports RowMapper automatic installation. - -* If you were using binding annotations then: - - `@BindingAnnotation` -> `@SqlStatementCustomizingAnnotation` - - `BindingFactory` -> `SqlStatementCustomizerFactory` - -* Sql object proxies must be interfaces now (jdbi3 restriction). But as java 8 interfaces support default methods, -its not a big problem - - instead of field injection (to access other proxies), now getter annotated with @Inject must be used. - -See [jdbi3 migration gude](http://jdbi.org/#_upgrading_from_v2_to_v3) for other (pure jdbi related) differences \ No newline at end of file diff --git a/src/doc/docs/guide/events.md b/src/doc/docs/guide/events.md deleted file mode 100644 index f72a62706..000000000 --- a/src/doc/docs/guide/events.md +++ /dev/null @@ -1,142 +0,0 @@ -# Guicey lifecycle events - -Guicey broadcast lifecycle events in all major points. Each event -provides access to all available state at this point. - -Events could be used for configuration analysis, reporting or to add some special -post processing for configuration items (e.g. post process modules before injector creation). - -!!! important - Event listeners could not modify configuration itself - (can't add new extensions, installers, bundles or disable anything). - -## Events - -All events are listed in `GuiceyLifecycle` enum (in execution order). - - -Event | Description | Possible usage -------|---------------|--------------- -**Dropwizard initialization phase** | | -ConfigurationHooksProcessed^**?**^ | Called after all registered hooks processing. Not called when no hooks used. | Only for info -DropwizardBundlesInitialized^**?**^ | Called after dropwizard bundles initialization (for dropwizard bundles registered through guicey api). Not called if no bundles were registered. | Logging, bundle instances modification (to affect run method) -BundlesFromLookupResolved^**?**^ | Called after resolution bundles through lookup mechanism. Not called if no bundles found. | Logging or post processing of found bundles. -BundlesResolved | Called with all known top-level bundles (transitive bundles are not yet known). Always called to indicate configuration state. | Could be used to modify top-level bundle instances -BundlesInitialized^**?**^ | Called after all bundles initialization (including transitive, so list of bundles could be bigger). Not called when no bundles registered. | Logging, post processing -CommandsResolved^**?**^ | Called if commands search is enabled and at least one command found | Logging -InstallersResolved | Called when all configured (and resolved by classpath scan) installers initialized | Potentially could be used to configure installer instances -ManualExtensionsValidated^**?**^ | Called when all manually registered extension classes are recognized by installers (validated). But only extensions, known to be enabled at that time are actually validated (this way it is possible to exclude extensions for non existing installers). Called only if at least one manual extension registered. | Logging, assertions -ClasspathExtensionsResolved^**?**^ | Called when classes from classpath scan analyzed and all extensions detected (if extension is also registered manually it would be also counted as from classpath scan). Called only if classpath scan is enabled and at least one extension detected. | Logging, assertions -Initialized | Meta event, called after GuiceBundle initialization (most of configuration done). Pure marker event, indicating guicey work finished under dropwizard configuration phase. | Last chance to modify Bootstrap -**Dropwizard run phase** | | -BeforeRun | Meta event, called before any guicey actions just to indicate first point where Environment, Configuration and introspected configuration are available | For example, used by `bundle.printConfigurationBindings()` to print configuration bindings before injector start (help with missed bindings debug) | -BundlesStarted^**?**^ | Called after bundles start (run method call). Not called if no bundles were used at all. Called only if bindings analysis is not disabled. | Logging -ModulesAnalyzed | Called after guice modules analysis and repackaging. Reveals all detected extensions and removed bindings info. | Logging, analysis validation logic -ExtensionsResolved | Called to indicate all enabled extensions (manual, from classpath scan and modules). Always called to indicate configuration state. | Logging or remembering list of all enabled extensions (classes only) -InjectorCreation | Called just before guice injector creation. Provides all configured modules (main and override) and all disabled modules. Always called. | Logging. Note that it is useless to modify module instance here, because they were already processed. -**Guice injector created** | | -ExtensionsInstalledBy | Called when installer installed all related extensions (for each installer) and only for installers actually performed installations (extensions list never empty). Note: jersey extensions are processed later. | Logging of installed extensions. Extension instance could be obtained from injector and post processed. -ExtensionsInstalled^**?**^ | Called after all installers install related extensions. Not called when no installed extensions (nothing registered or all disabled) | Logging or extensions post processing -ApplicationRun | Meta event, called when guice injector started, extensions installed (except jersey extensions because neither jersey nor jetty is't start yet) and all guice singletons initialized. At this point injection to registered commands is performed (this may be important if custom command run application instead of "server"). Point is just before `Application.run` method. | Ideal point for jersey and jetty listeners installation (with shortcut methods in event). -**Jersey initialization** | | -JerseyConfiguration | Jersey context starting. Both jersey and jetty are starting. | First point where jersey's `InjectionManager` (and `ServiceLocator`) become available -JerseyExtensionsInstalledBy | Called when jersey installer installed all related extensions (for each installer) and only for installers actually performed installations (extensions list never empty) | Logging of installed extensions. Extension instance could be obtained from injector/locator and post processed. -JerseyExtensionsInstalled^**?**^ | Called after all jersey installers install related extensions. Not called when no installed extensions (nothing registered or all disabled). At this point HK2 is not completely started yet (and so extensions) | Logging or extensions post processing -ApplicationStarted | Meta event, called after complete dropwizard startup. This event also will be fired in guicey lightweight tests | May be used as assured "started" point (after all initializations). For example, for reporting. -ApplicationShutdown | Meta event, called on server shutdown start. This event also will be fired in guicey lightweight tests | May be used for shutdown logic. -ApplicationStoppedEvent | Meta event, called after application shutdown. This event also will be fired in guicey lightweight tests | May be used in rare cases to cleanup fs resources after application stop. - -^?^ - event may not be called - -## Listeners - -Events listener registration: - -```java -GuiceBundle.builder() - .listen(new MyListener(), new MyOtherListener()) - ... - .build() -``` - -!!! note - Listeners could be also registered in guicey bundle, but they will not receive all events: - - * `>= BundlesInitialized` for listeners registered in initialization method - * `>= BundlesStarted` for listeners registered in run method - -Event listener could implement generic event interface `GuiceyLifecycleListener` and use -enum to differentiate required events: - -```java -public class MyListener implements GuiceyLifecycleListener { - - public void onEvent(GuiceyLifecycleEvent event) { - switch (event.getType()) { - case InjectorCreation: - InjectorCreationEvent e = (InjectorCreationEvent) event; - ... - } - } -} -``` - -Or use `GuiceyLifecycleAdapter` adapter and override only required methods: - -```java -public class MyListener extends GuiceyLifecycleAdapter { - - @Override - protected void injectorCreation(final InjectorCreationEvent event) { - ... - } -} -``` - -!!! tip - In `ApplicationStarted` and `ApplicationShutdown` events lightweight guicey test - environment may be differentiated from real server startup with `.isJettyStarted()` method. - -### De-duplication - -Event listeners are also support de-duplication to prevent unnecessary duplicates usage -(for example, two bundles may register one listener because they are not always used together). -But it is **not the same mechanism** as configuration items de-duplication. - -Simply listeners are registered in the `LinkedHashSet` and so listeners could control de-duplication -with a proper `equals` and `hashCode` implementations - -Many reports use this feature (because all of them are based on listeners). For example, -[diagnostic report](diagnostic/configuration-report.md) use the following implementations: - -```java -@Override -public boolean equals(final Object obj) { - // allow only one instance with the same title - return obj instanceof ConfigurationDiagnostic - && reportTitle.equals(((ConfigurationDiagnostic) obj).reportTitle); -} - -@Override -public int hashCode() { - return reportTitle.hashCode(); -} -``` - -And with it, `.printDiagnosticInfo()` can be called multiple times and still only one report -will be actually printed. - -### Events hierarchy - -All event classes inherit from some base event classes. Base event classes are extending each other: -as lifecycle phases go, more objects become available. So you can access any available (at this point) object -from event instance. - -Base event | Description ------------|------------- -GuiceyLifecycleEvent | The lowest event type. Provides access to event type and options. -ConfigurationPhaseEvent | Initialization phase event. Provides access to Bootstrap. -RunPhaseEvent | Dropwizard run phase. Provides access to Configuration, ConfigurationTree, Environment. Shortcut for configuration bindings report renderer -InjectorPhaseEvent | Guice injector created. Available injector and GuiceyCofigurationInfo (guicey configuration). Shortcuts for configuration reports renderer -JerseyPhaseEvent | Jersey starting. Jersey's `InjectionManager` available. - \ No newline at end of file diff --git a/src/doc/docs/guide/test/general.md b/src/doc/docs/guide/test/general.md deleted file mode 100644 index 08cb78535..000000000 --- a/src/doc/docs/guide/test/general.md +++ /dev/null @@ -1,276 +0,0 @@ -# General test tools - -Test framework-agnostic tools. -Useful when: - - - There is no extensions for your test framework - - Assertions must be performed after test app shutdown (or before startup) - -Test utils: - - - `TestSupport` - root utilities class, providing easy access to other helpers - - `DropwizardTestSupport` - [dropwizard native support](https://www.dropwizard.io/en/release-2.0.x/manual/testing.html#non-junit) for full integration tests - - `GuiceyTestSupport` - guice context-only integration tests (without starting web part) - - `ClientSupport` - web client helper (useful for calling application urls) - -!!! important - `TestSupport` assumed to be used as a universal shortcut: everything could be created/executed through it - so just type `TestSupport.` and look available methods - no need to remember other classes. - -## Web app - -Core [DropwizardTestSupport](https://www.dropwizard.io/en/release-2.0.x/manual/testing.html#non-junit) class -used to run complete application for testing: - -```java -DropwizardTestSupport support = TestSupport.webApp(App.class, "path/to/test-config.yml"); - // OR for custom configuration - new DropwizardTestSupport(App.class, "path/to/test-config.yml"); -// start -support.before(); - -// helpers -support.getEnvironment(); -support.getConfiguration(); -support.getApplication(); - -// stop -support.after(); -``` - -Provides two lifecycle methods: `before()` and `after()` and utilities to access context environment and configuration objects. - -See constructor for advanced configuration options. - -## Core app - -`GuiceyTestSupport` is an inheritor of `DropwizardTestSupport` (could be casted) starting only -guice context without web part. Provides additional utility methods: - -```java -GuiceyTestSupport support = TestSupport.coreApp(App.class, "path/to/test-config.yml"); - // OR for custom configuration - new GuiceyTestSupport(App.class, "path/to/test-config.yml"); -// start -support.before(); - -// additional method -support.getBean(Key/Class); - -// stop -support.after(); -``` - -Also provide shortcut `.run(callback)` method as an alternative to manual `before()` and `after()` calls. - -## Client - -`ClientSupport` is a [JerseyClient](https://eclipse-ee4j.github.io/jersey.github.io/documentation/2.29.1/client.html) -aware of dropwizard configuration, so you can easily call admin/main/rest urls. - -Creation: - -```java -ClientSupport client = TestSupport.webClient(support); -``` - -where support is `DropwizardTestSupport` or `GuiceyTestSupport` (in later case it could be used only as generic client for calling external urls). - -Example usage: - -```java -// GET {rest path}/some -client.targetRest("some").request().buildGet().invoke() - -// GET {main context path}/servlet -client.targetMain("servlet").request().buildGet().invoke() - -// GET {admin context path}/adminServlet -client.targetAdmin("adminServlet").request().buildGet().invoke() - -// General external url call -client.target("https://google.com").request().buildGet().invoke() -``` - -!!! tip - All methods above accepts any number of strings which would be automatically combined into correct path: - ```groovy - client.targetRest("some", "other/", "/part") - ``` - would be correctly combined as "/some/other/part/" - -As you can see, test code is abstracted from actual configuration: it may be default or simple server -with any contexts mapping on any ports - target urls will always be correct. - -```java -Response res = client.targetRest("some").request().buildGet().invoke() - -Assertions.assertEquals(200, res.getStatus()) -Assertions.assertEquals("response text", res.readEntity(String)) -``` - -Also, if you want to use other client, client object can simply provide required info: - -```groovy -client.getPort() // app port (8080) -client.getAdminPort() // app admin port (8081) -client.basePathMain() // main context path (http://localhost:8080/) -client.basePathAdmin() // admin context path (http://localhost:8081/) -client.basePathRest() // rest context path (http://localhost:8080/) -``` - -## TestSupport - -`TestSupport` class simplifies usage of all test utilities: it is the only class you'll need -to remember - all other utilities could be created (or found) through it. - -Methods inside it follow convention: - -- `webApp` - complete application (`DropwizardTestSupport`) -- `coreApp` - guice-only part (`GuiceyTestSupport`) - -Methods: - -- `runWebApp(Class, String, Callback?)` -- `runCoreApp(Class, String, Callback?)` -- `webApp(Class, String)` -- `coreAoo(Class, String)` -- `webClient(support)` -- `getInjector(support)` -- `getBean(support, Key/Class)` -- `injectBeans(support, target)` -- `run(support, Callback)` - -where `support` is an instance of `DropwizardTestSupport` (or `GuiceyTestSupport`). - -### Simple run - -If application must be just started and stopped (e.g. to test startup errors): - -```java -TestSupport.runWebApp(App.class, "path/to/test-config.yml"); -``` - -or without configuration: - -```java -TestSupport.runWebApp(App.class, null); -``` - -More advanced version, using callback: - -```java -TestSupport.runWebApp(App.class, "path/to/test-config.yml", (injector) -> { - injector.getInstance(MyService.class).doSomething(); -}); -``` - -Here we get injector object inside callback and can call any guice service and perform any assertions while application runs. -You can also use it to return a value: - -```java -String value = TestSupport.runWebApp(App.class, "path/to/test-config.yml", (injector) -> { - return injector.getInstance(MyService.class).computeValue() -}); -``` - -All the same is available for lightweight guice-only testing: - -```java -TestSupport.runCoreApp(App.class, "path/to/test-config.yml", (injector) -> { - injector.getInstance(MyService.class).doSomething(); -}); -``` - -### Advanced usage - -For more advanced usage, you'll need to construct `DropwizardTestSupport` or `GuiceyTestSupport` objects first: - -```java -DropwizardTestSupport support = TestSupport.webApp(App.class, "path/to/test-config.yml"); -``` - -or - -```java -GuiceyTestSupport support = TestSupport.coreApp(App.class, "path/to/test-config.yml"); -``` - -Configuration path could be null: - -```java -DropwizardTestSupport support = TestSupport.webApp(App.class, null); -``` - -!!! note - This construction is suitable for the simplest cases, but you can always create - `DropwizardTestSupport` object manually. - - Utility call just hides easy to forget no-config instantiation: - - ```java - new DropwizardTestSupport(App.class, (String) null); - ``` - -Instead of manual lifecycle methods call you can use: - -```java -TestSupport.run(support, (injector) -> { - // do somthing while app started -}) -``` - -Inside callback the following shortcuts could be used: - -- `TestSupport.getInjector(support)` -- `TestSupport.getBean(support, Key/Class)` -- `TestSupport.injectBeans(support, target)` - -Complete example using junit: - -```java -public class RawTest { - - static DropwizardTestSupport support; - - @Inject MyService service; - - @BeforeAll - public static void setup() { - support = TestSupport.coreApp(App.class, null); - // support = TestSupport.webApp(App.class, null); - // start app - support.before(); - } - - @BeforeEach - public void before() { - // inject services in test - TestSupport.injectBeans(support, this); - } - - @AfterAll - public static void cleanup() { - if (support != null) { - support.after(); - } - } - - @Test - public void test() { - Assertions.assertEquals("10", service.computeValue()); - } -} -``` - -## Special needs - -If you need to: - -* Intercept application exit (e.g. startup crash) -* Validate system output (e.g. logs correctness) -* Change environment or system variables (with reset) - -Then use [system stubs](https://github.com/webcompere/system-stubs) library -(low-level usage is described in project readme). - -There is also specialized guides for [junit 5](junit5.md#dropwizard-startup-error) -and [spock 2](spock2.md#special-cases). \ No newline at end of file diff --git a/src/doc/docs/guide/test/junit5.md b/src/doc/docs/guide/test/junit5.md deleted file mode 100644 index c58ece684..000000000 --- a/src/doc/docs/guide/test/junit5.md +++ /dev/null @@ -1,1072 +0,0 @@ -# JUnit 5 - -!!! note "" - [Migration from JUnit 4](junit4.md#migrating-to-junit-5) - -Junit 5 [user guide](https://junit.org/junit5/docs/current/user-guide/) - -## Setup - -You will need the following dependencies (assuming BOM used for versions management): - -```groovy -testImplementation 'io.dropwizard:dropwizard-testing' -testImplementation 'org.junit.jupiter:junit-jupiter-api' -testRuntimeOnly 'org.junit.jupiter:junit-jupiter' -``` - -!!! tip - If you already have junit4 or spock tests, you can activate [vintage engine](https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4) - so all tests could work **together** with junit 5: - ```groovy - testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' - ``` - -!!! note - In gradle you need to explicitly [activate junit 5 support](https://docs.gradle.org/current/userguide/java_testing.html#using_junit5) with - ```groovy - test { - useJUnitPlatform() - ... - } - ``` - -!!! warning - Junit 5 annotations are **different** from junit4, so if you have both junit 5 and junit 4 - make sure correct classes (annotations) used for junit 5 tests: - ```java - import org.junit.jupiter.api.Assertions; - import org.junit.jupiter.api.Test; - ``` - -## Dropwizard extensions compatibility - -Guicey extensions can be used with dropwizard extensions. But this may be required only in edge cases -when multiple applications startup is required. - -!!! info - There is a difference in extensions implementation. - - Dropwizard extensions work as: - junit extension `@ExtendWith(DropwizardExtensionsSupport.class)` looks for fields - implementing `DropwizardExtension` (like `DropwizardAppExtension`) and start/stop them according to test lifecycle. - - Guicey extensions completely implemented as junit extensions (and only hook fields are manually searched). - Also, guciey extension rely on junit parameters injection. Both options has pros and cons. - - -## Extensions - -Provided extensions: - -* `@TestGuiceyApp` - for lightweight tests (without starting web part, only guice context) -* `@TestDropwizardApp` - for complete integration tests - -Both extensions allow using injections directly in test fields. - -Extensions are compatible with [parallel execution](#parallel-execution) (no side effects). - -[Alternative declaration](#alternative-declaration) might be used for [deferred configuration](#deferred-configuration) -or [starting application for each test method](#start-application-by-test-method). - -Pre-configured [http client](#client) might be used for calling testing application endpoints. - -!!! note - You can use junit 5 extensions with [Spock 2](spock2.md) - -Test environment might be prepared with [setup objects](#test-environment-setup) -and application might be re-configured with [hooks](#application-test-modification) - -## Testing core logic - -`@TestGuiceyApp` runs all guice logic without starting jetty (so resources, servlets and filters will not be available). -`Managed` objects will still be handled correctly. - -```java -@TestGuiceyApp(MyApplication.class) -public class AutoScanModeTest { - - @Inject - MyService service; - - @Test - public void testMyService() { - Assertions.assertEquals("hello", service.getSmth()); - } -``` - -Also, injections work as method parameters: - -```java -@TestGuiceyApp(MyApplication.class) -public class AutoScanModeTest { - - public void testMyService(MyService service) { - Assertions.assertEquals("hello", service.getSmth()); - } -``` - -Application started before all tests in annotated class and stopped after them. - -## Testing web logic - -`@TestDropwizardApp` is useful for complete integration testing (when web part is required): - -```groovy -@TestDropwizardApp(MyApplication.class) -class WebModuleTest { - - @Inject - MyService service - - @Test - public void checkWebBindings(ClientSupport client) { - - Assertions.assertEquals("Sample filter and service called", - client.targetMain("servlet").request().buildGet().invoke().readEntity(String.class)); - - Assertions.assertTrur(service.isCalled()); -``` - -### Random ports - -In order to start application on random port you can use configuration shortcut: - -```groovy -@TestDropwizardApp(value = MyApplication.class, randomPorts = true) -``` - -!!! note - Random ports will be applied even if configuration with exact ports provided: - ```groovy - @TestDropwizardApp(value = MyApplication, - config = 'path/to/my/config.yml', - randomPorts = true) - ``` - Also, random ports support both server types (default and simple) - -Real ports could be resolved with [ClientSupport](#client) object. - -### Rest mapping - -Normally, rest mapping configured with `server.rootMapping=/something/*` configuration, but -if you don't use custom configuration class, but still want to re-map rest, shortcut could be used: - -```groovy -@TestDropwizardApp(value = MyApplication.class, restMapping="something") -``` - -In contrast to config declaration, attribute value may not start with '/' and end with '/*' - -it would be appended automatically. - -This option is only intended to simplify cases when custom configuration file is not yet used in tests -(usually early PoC phase). It allows you to map servlet into application root in test (because rest is no -more resides in root). When used with existing configuration file, this parameter will override file definition. - -## Guice injections - -Any gucie bean may be injected directly into test field: - -```groovy -@Inject -SomeBean bean -``` - -This may be even bean not declared in guice modules (JIT injection will occur). - -To better understand injection scopes look the following test: - -```groovy -@TestGuiceyApp(AutoScanApplication.class) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class InjectionScopeTest { - - // new instance injected on each test - @Inject - TestBean bean; - - // the same context used for all tests (in class), so the same bean instance inserted before each test - @Inject - TestSingletonBean singletonBean; - - @Test - @Order(1) - public void testInjection() { - bean.value = 5; - singletonBean.value = 15; - - Assertions.assertEquals(5, bean.value); - Assertions.assertEquals(15, singletonBean.value); - - } - - @Test - @Order(2) - public void testSharedState() { - - Assertions.assertEquals(0, bean.value); - Assertions.assertEquals(15, singletonBean.value); - } - - // bean is in prototype scope - public static class TestBean { - int value; - } - - @Singleton - public static class TestSingletonBean { - int value; - } -} -``` - - -!!! note - Guice AOP will not work on test methods (because test instances not created by guice). - -## Parameter injection - -Any **declared** guice bean may be injected as method parameter: - -```java -@Test -public void testSomthing(DummyBean bean) -``` - -(where `DummyBean` is manually declared in some module or JIT-instantiated during injector creation). - -For not declared beans injection (JIT) special annotation must be used: - -```java -@Test -public void testSomthing(@Jit TestBean bean) -``` - -!!! info - Additional annotation required because you may use other junit extensions providing their own - parameters, which guicey extension should not try to handle. That's why not annotated parameters - verified with existing injector bindings. - -Qualified and generified injections will also work: - -```java -@Test -public void testSomthing(@Named("qual") SomeBean bean, - TestBean generifiedBean, - Provider provider) -``` - -Also, there are special objects available as parameters: - -* `Application` or exact application class (`MyApplication`) -* `ObjectMapper` -* `ClientSupport` application web client helper - -!!! note - Parameter injection will work not only in test, but also in lifecyle methods (beforeAll, afterEach etc.) - -Example: - -```java -@TestDropwizardApp(AutoScanApplication.class) -public class ParametersInjectionDwTest { - - public ParametersInjectionDwTest(Environment env, DummyService service) { - Preconditions.checkNotNull(env); - Preconditions.checkNotNull(service); - } - - @BeforeAll - static void before(Application app, DummyService service) { - Preconditions.checkNotNull(app); - Preconditions.checkNotNull(service); - } - - @BeforeEach - void setUp(Application app, DummyService service) { - Preconditions.checkNotNull(app); - Preconditions.checkNotNull(service); - } - - @AfterEach - void tearDown(Application app, DummyService service) { - Preconditions.checkNotNull(app); - Preconditions.checkNotNull(service); - } - - @AfterAll - static void after(Application app, DummyService service) { - Preconditions.checkNotNull(app); - Preconditions.checkNotNull(service); - } - - @Test - void checkAllPossibleParams(Application app, - AutoScanApplication app2, - Configuration conf, - TestConfiguration conf2, - Environment env, - ObjectMapper mapper, - Injector injector, - ClientSupport client, - DummyService service, - @Jit JitService jit) { - assertNotNull(app); - assertNotNull(app2); - assertNotNull(conf); - assertNotNull(conf2); - assertNotNull(env); - assertNotNull(mapper); - assertNotNull(injector); - assertNotNull(client); - assertNotNull(service); - assertNotNull(jit); - assertEquals(client.getPort(), 8080); - assertEquals(client.getAdminPort(), 8081); - } - - public static class JitService { - - private final DummyService service; - - @Inject - public JitService(DummyService service) { - this.service = service; - } - } -} -``` - -## Client - -Both extensions prepare special jersey client instance which could be used for web calls. -It is mostly useful for complete web tests to call rest services and servlets. - -```java -@Test -void checkRandomPorts(ClientSupport client) { - Assertions.assertNotEquals(8080, client.getPort()); - Assertions.assertNotEquals(8081, client.getAdminPort()); -} -``` - -Client object provides: - -* Access to [JerseyClient](https://eclipse-ee4j.github.io/jersey.github.io/documentation/2.29.1/client.html) object (for raw calls) -* Shortcuts for querying main, admin or rest contexts (it will count the current configuration automatically) -* Shortcuts for base main, admin or rest contexts base urls (and application ports) - -Example usages: - -```java -// GET {rest path}/some -client.targetRest("some").request().buildGet().invoke() - -// GET {main context path}/servlet -client.targetMain("servlet").request().buildGet().invoke() - -// GET {admin context path}/adminServlet -client.targetAdmin("adminServlet").request().buildGet().invoke() -``` - -!!! tip - All methods above accepts any number of strings which would be automatically combined into correct path: - ```groovy - client.targetRest("some", "other/", "/part") - ``` - would be correctly combined as "/some/other/part/" - -As you can see test code is abstracted from actual configuration: it may be default or simple server -with any contexts mapping on any ports - target urls will always be correct. - -```java -Response res = client.targetRest("some").request().buildGet().invoke() - -Assertions.assertEquals(200, res.getStatus()) -Assertions.assertEquals("response text", res.readEntity(String)) -``` - -Also, if you want to use other client, client object can simply provide required info: - -```groovy -client.getPort() // app port (8080) -client.getAdminPort() // app admin port (8081) -client.basePathMain() // main context path (http://localhost:8080/) -client.basePathAdmin() // admin context path (http://localhost:8081/) -client.basePathRest() // rest context path (http://localhost:8080/) -``` - -Raw client usage: - -```java -// call completely external url -client.target("http://somedomain:8080/dummy/").request().buildGet().invoke() -``` - -!!! warning - Client object could be injected with both dropwizard and guicey extensions, but in case of guicey extension, - only raw client could be used (because web part not started all other methods will throw NPE) - -## Configuration - -For both extensions you can configure application with external configuration file: - -```java -@TestGuiceyApp(value = MyApplication.class, - config = "path/to/my/config.yml" -public class ConfigOverrideTest { -``` - -Or just declare required values: - -```java -@TestGuiceyApp(value = MyApplication.class, - configOverride = { - "foo: 2", - "bar: 12" - }) -public class ConfigOverrideTest { -``` - -(note that overriding declaration follows yaml format "key: value") - -Or use both at once (here overrides will override file values): - -```java -@TestGuiceyApp(value = MyApplication.class, - config = 'path/to/my/config.yml', - configOverride = { - "foo: 2", - "bar: 12" - }) -class ConfigOverrideTest { -``` - -### Deferred configuration - -If you need to configure value, supplied by some other extension, or value may be resolved only -after test start, then static overrides declaration is not an option. In this case use -[alternative extensions declaration](#alternative-declaration) which provides additional -config override methods: - -```java -@RegisterExtension -@Order(1) -static FooExtension ext = new FooExtension(); - -@RegisterExtension -@Order(2) -static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) - .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") - .configOverrides("foo: 1") - .configOverride("bar", () -> ext.getValue()) - .configOverrides(new ConfigOverrideValue("baa", () -> "44")) - .create(); -``` - -In most cases `configOverride("bar", () -> ext.getValue())` would be enough to configure a supplier instead -of static value. - -In more complex cases, you can use custom implementations of `ConfigOverride`. - -!!! warning "" - Guicey have to accept only `ConfigOverride` objects implementing custom - `ru.vyarus.dropwizard.guice.test.util.ConfigurablePrefix` interface. - In order to support parallel tests guicey generates unique config prefix for each test - (because all overrides eventually stored to system properties) and so it needs a way - to set this prefix into custom `ConfigOverride` objects. - -### Configuration from 3rd party extensions - -If you have junit extension (e.g. which starts db for test) and you need -to apply configuration overrides from that extension, then you should simply -store required values inside junit storage: - -```java -public class ConfigExtension implements BeforeAllCallback { - - @Override - public void beforeAll(ExtensionContext context) throws Exception { - // do something and then store value - context.getStore(ExtensionContext.Namespace.GLOBAL).put("ext1", 10); - } -} -``` - -And map overrides directly from store using `configOverrideByExtension` method: - -```java -@ExtendWith(ConfigExtension.class) -public class SampleTest { - - @RegisterExtension - static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) - .configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1") - .create(); -} -``` - -Here, value applied by extension under key `ext1` would be applied to configuration `ext1` path. -If you need to use different configuration key: - -```java -.configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1", "key") -``` - -!!! tip - You can use [setup objects](#test-environment-setup) instead of custom junit extensions for test environment setup - -## Test environment setup - -It is often required to prepare test environment before starting dropwizard application. -Normally, such cases require writing custom junit extensions. In order to simplify -environment setup, guicey provides `TestEnviromentSetup` interface. - -Setup objects are called before application startup and could directly apply (through parameter) -configuration overrides and hooks. - -For example, suppose you need to setup database before test: - -```java -public class TestDbSetup implements TestEnvironmentSetup { - - @Override - public Object setup(TestExtension extension) { - // pseudo code - Db db = DbFactory.startTestDb(); - // register required configuration - extension - .configOverride("database.url", ()-> db.getUrl()) - .configOverride("database.user", ()-> db.getUser()) - .configOverride("database.password", ()-> db.getPassword); - // assuming object implements Closable - return db; - } -} -``` - -It is not required to return anything, only if something needs to be closed after application shutdown: -objects other than `Closable` (`AutoClosable`) or `org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource` -simply ignored. -This approach (only one method) simplifies interface usage with lambdas. - -Setup object might be declared in extension annotation: - -```java -@TestGuiceyApp(value=App.class, setup=TestDbSetup.class) -``` - -Or in manual registration: - -```java -@RegisterExtension -TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class) - // as class - .setup(TestDbSetup.class) - // or as instance - .setup(new TestDbSetup()) -``` - -Or with lambda: - -```java -.setup(ext -> { - Db db = new Db(); - ext.configOverride("db.url", ()->db.getUrl()) - return db; -}) -``` - -### Setup fields - -Alternatively, setup objects might be declared simply in test fields: - -```java -@EnableSetup -static TestEnvironmentSetup db = ext -> { - Db db = new Db(); - ext.configOverride("db.url", ()->db.getUrl()) - return db; - }; -``` - -or - -```java -@EnableSetup -static TestDbSetup db = new TestDbSetup() -``` - -This could be extremely useful if you need to unify setup logic for multiple tests, -but use different extension declarations in test. In this case simply move field -declaration into base test class: - -```java -public abstract class BaseTest { - - @EnableSetup - static TestDbSetup db = new TestDbSetup(); -} -``` - -!!! note - To avoid confusion with guicey hooks: setup object required to prepare test environment before test (and apply - required configurations) whereas hooks is a general mechanism for application customization (not only in tests). - Setup objects are executed before application startup (before `DropwizardTestSupport` object creation) and hooks - are executed by started application. - -## Application test modification - -You can use [hooks to customize application](overview.md#configuration-hooks). - -In both extensions annotation hooks could be declared with attribute: - -```java -@TestDropwizardApp(value = MyApplication.class, hooks = MyHook.class) -``` - -or - -```java -@TestGuiceyApp(value = MyApplication.class, hooks = MyHook.class) -``` - -Where MyHook is: - -```java -public class MyHook implements GuiceyConfigurationHook {} -``` - -### Hook fields - -Alternatively, you can declare hook directly in test field: - -```java -@EnableHook -static GuiceyConfigurationHook HOOK = builder -> builder.modules(new DebugModule()); -``` - -Any number of fields could be declared. The same way hook could be declared in base test class: - -```java -public abstract class BaseTest { - - // hook in base class - @EnableHook - static GuiceyConfigurationHook BASE_HOOK = builder -> builder.modules(new DebugModule()); -} - -@TestGuiceyApp(value = App.class, hooks = SomeOtherHook.class) -public class SomeTest extends BaseTest { - - // Another hook - @EnableHook - static GuiceyConfigurationHook HOOK = builder -> builder.modules(new DebugModule2()); -} -``` - -All 3 hooks will work. - -## Extension configuration unification - -It is a common need to run multiple tests with the same test application configuration -(same config overrides, same hooks etc.). -Do not configure it in each test, instead move extension configuration into base test class: - -```java -@TestGuiceyApp(...) -public abstract class AbstractTest { - // here might be helper methods -} -``` - -And now all test classes should simply extend it: - -```java -public class Test1 extends AbstractTest { - - @Inject - MyService service; - - @Test - public void testSomething() { ... } -} -``` - -If you use manual extension configuration (through field), just replace annotation in base class with -manual declaration - approach would still work. - -## Parallel execution - -Junit [parallel tests execution](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution) -could be activated with properties file `junit-platform.properties` located at test resources root: - -```properties -junit.jupiter.execution.parallel.enabled = true -junit.jupiter.execution.parallel.mode.default = concurrent -``` - -!!! note - In order to avoid config overriding collisions (because all overrides eventually stored to system properties) - guicey generates unique property prefixes in each test. - -To avoid port collisions in dropwizard tests use [randomPorts option](#random-ports). - -## Alternative declaration - -Both extensions could be declared in fields: - -```java -@RegisterExtension -static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(AutoScanApplication.class) - .config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml") - .configOverrides("foo: 2", "bar: 12") - .randomPorts() - .hooks(Hook.class) - .hooks(builder -> builder.disableExtensions(DummyManaged.class)) - .create(); -``` - -The only difference with annotations is that you can declare hooks and setup objects as lambdas directly -(still hooks in static fields will also work). - -```java -@RegisterExtension -static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) - ... -``` - -This alternative declaration is intended to be used in cases when guicey extensions need to be aligned with -other 3rd party extensions: in junit you can order extensions declared with annotations (by annotation order) -and extensions declared with `@RegisterExtension` (by declaration order). But there is no way -to order extension registered with `@RegisterExtension` before annotation extension. - -So if you have 3rd party extension which needs to be executed BEFORE guicey extensions, you can use field declaration. - -!!! note - Junit 5 intentionally shuffle `@RegisterExtension` extensions order, but you can always order them with - `@Order` annotation. - -### Start application by test method - -When you declare extensions with annotations or with `@RegisterExtension` in static fields, -application would be started before all test methods and shut down after last test method. - -If you want to start application *for each test method* then delcare extension in non-static field: - -```java -RegisterExtension -TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() - -// injection would be re-newed for each test method -@Inject Bean bean; - -@Test -public void test1() { - Assertions.assertEquals(0, bean.value); - // changing value to show that bean was reset between tests - bean.value = 10 -} - -@Test -public void test2() { - Assertions.assertEquals(0, bean.value); - bean.value = 10 -} -``` - -Also, `@EnableHook` and `@EnableSetup` fields might also be not static (but static fields would also work) in this case: - -```java -@RegisterExtension -TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(App.class).create() - -@EnableSetup -MySetup setup = new MySetup() -``` - -## Junit nested classes - -Junit natively supports [nested tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested). - -Guicey extensions affects all nested tests below declaration (nesting level is not limited): - -```java -@TestGuiceyApp(AutoScanApplication.class) -public class NestedPropagationTest { - - @Inject - Environment environment; - - @Test - void checkInjection() { - Assertions.assertNotNull(environment); - } - - @Nested - class Inner { - - @Inject - Environment env; // intentionally different name - - @Test - void checkInjection() { - Assertions.assertNotNull(env); - } - } -} -``` - -!!! note - Nested tests will use exactly the same guice context as root test (application started only once). - -Extension declared on nested test will affect all sub-tests: - -```java -public class NestedTreeTest { - - @TestGuiceyApp(AutoScanApplication.class) - @Nested - class Level1 { - - @Inject - Environment environment; - - @Test - void checkExtensionApplied() { - Assertions.assertNotNull(environment); - } - - @Nested - class Level2 { - @Inject - Environment env; - - @Test - void checkExtensionApplied() { - Assertions.assertNotNull(env); - } - - @Nested - class Level3 { - - @Inject - Environment envr; - - @Test - void checkExtensionApplied() { - Assertions.assertNotNull(envr); - } - } - } - } - - @Nested - class NotAffected { - @Inject - Environment environment; - - @Test - void extensionNotApplied() { - Assertions.assertNull(environment); - } - } -} -``` - -This way nested tests allows you to use different extension configurations in one (root) class. - -Note that extension declaration with `@RegisterExtension` on the root class field would also -be applied to nested tests. Even declaration in non-static field (start application for each method) -would also work. - -### Use interfaces to share tests - -This is just a tip on how to execute same test method in different environments. - -```java -public class ClientSupportDwTest { - - interface ClientCallTest { - // test to apply for multiple environments - @Test - default void callClient(ClientSupport client) { - Assertions.assertEquals("main", client.targetMain("servlet") - .request().buildGet().invoke().readEntity(String.class)); - } - } - - @TestDropwizardApp(App.class) - @Nested - class DefaultConfig implements ClientCallTest { - - @Test - void testClient(ClientSupport client) { - Assertions.assertEquals("http://localhost:8080/", client.basePathMain()); - } - } - - @TestDropwizardApp(value = App.class, configOverride = { - "server.applicationContextPath: /app", - "server.adminContextPath: /admin", - }, restMapping = "api") - @Nested - class ChangedDefaultConfig implements ClientCallTest { - - @Test - void testClient(ClientSupport client) { - Assertions.assertEquals("http://localhost:8080/app/", client.basePathMain()); - } - } -} -``` - -Here test declared in `ClientCallTest` interface will be called for each nested test -(one declaration - two executions in different environments). - -## Meta annotation - -You can prepare meta annotation (possibly combining multiple 3rd party extensions): - -```java -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@TestDropwizardApp(AutoScanApplication.class) -public @interface MyApp { -} - -@MyApp -public class MetaAnnotationDwTest { - - @Test - void checkAnnotationRecognized(Application app) { - Assertions.assertNotNull(app); - } -} -``` - -OR you can simply use base test class and configure annotation there: - -```java -@TestDropwizardApp(AutoScanApplication.class) -public class BaseTest {} - -public class ActualTest extends BaseTest {} -``` - -## Dropwizard startup error - -!!! warning - Tests written in such way CAN'T run in parallel due to `System.*` modifications. - -To test application startup fails you can use [system stubs](https://github.com/webcompere/system-stubs) library - -```groovy -testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.0.1' -``` - -Testing app startup fail: - -```java -@ExtendWith(SystemStubsExtension.class) -public class MyTest { - @SystemStub - SystemExit exit; - @SystemStub - SystemErr err; - - @Test - public void testStartupError() { - exit.execute(() -> new App().run('server')); - - Assertions.assertEquals(1, exit.getExitCode()); - Assertions.assertTrue(err.getTest().contains("Error message text")); - } -} -``` - -Note that you can also substitute environment variables and system properties and validate output: - -```java -@ExtendWith(SystemStubsExtension.class) -public class MyTest { - @SystemStub - EnvironmentVariables ENV; - @SystemStub - SystemOut out; - @SystemStub - SystemProperties propsReset; - - @BeforeAll - public void setup() { - ENV.set("VAR", "1"); - System.setProperty("foo", "bar"); // OR propsReset.set("foo", "bar") - both works the same - } - - @Test - public void test() { - // here goes some test that requires custom environment and system property values - - // validating output - Assertions.assertTrue(out.getTest().contains("some log message")); - } -} -``` - -Pay attention that there is no need for cleanup: system properties and environment variables would be re-set automatically! - -!!! note - Use [test framework-agnostic utilities](general.md) to run application with configuration or to run - application without web part (for faster test). - -## 3rd party extensions integration - -It is extremely simple in JUnit 5 to [write extensions](https://junit.org/junit5/docs/current/user-guide/#extensions). -If you do your own extension, you can easily integrate with guicey or dropwizard extensions: there -are special static methods allowing you to obtain main test objects: - -* `GuiceyExtensionsSupport.lookupSupport(extensionContext)` -> `Optional` -* `GuiceyExtensionsSupport.lookupInjector(extensionContext)` -> `Optional` -* `GuiceyExtensionsSupport.lookupClient(extensionContext)` -> `Optional` - -For example: - -```java -public class MyExtension implements BeforeEachCallback { - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - Injector injector = GuiceyExtensionsSupport.lookupInjector(context).get(); - ... - } -} -``` - -(guicey holds test state in junit test-specific storages and that's why test context is required) - -!!! warning - There is no way in junit to order extensions, so you will have to make sure that your extension - will be declared after guicey extension (`@TestGuiceyApp` or `@TestDropwizardApp`). - -There is intentionally no direct api for applying configuration overrides from -3rd party extensions because it would be not obvious. Instead, you should always -declare overridden value in extension declaration. Either use instance getter: - -```java -@RegisterExtension -static MyExtension ext = new MyExtension() - -@RegisterExtension -static TestGuiceyAppExtension dw = TestGuiceyAppExtension.forApp(App.class) - .configOverride("some.key", ()-> ext.getValue()) - .create() -``` - -Or store value [inside junit store](#configuration-from-3rd-party-extensions) and then reference it: - -```java -@RegisterExtension -static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class) - .configOverrideByExtension(ExtensionContext.Namespace.GLOBAL, "ext1") - .create(); -``` diff --git a/src/doc/docs/guide/test/overview.md b/src/doc/docs/guide/test/overview.md deleted file mode 100644 index 428cc7691..000000000 --- a/src/doc/docs/guide/test/overview.md +++ /dev/null @@ -1,209 +0,0 @@ -# Testing - -Guicey provides test extensions for: - -* [Spock 2](spock2.md) -* [JUnit 5](junit5.md) -* [Framework-agnostic utilities](general.md) - -Deprecated: - -* [Spock 1](spock.md) -* [JUnit 4](junit4.md) - -All extensions implemented with [DropwizardTestSupport](https://www.dropwizard.io/en/latest/manual/testing.html#non-junit). - -!!! note "History" - Spock 1 extensions were much more advanced than JUnit 4 rules, simply because - spock extensions model was much more powerful. - - JUnit 5 extension model is almost equal to spock and JUnit 5 extensions were - "an evolution" of the spock extensions. - - There is no special Spock 2 extensions, instead junit 5 extensions must be used directly. - You get the best of both worlds - use junit extensions (and so can always easily migrate to pure junit) and - have spock (and groovy) expressiveness. - - Currently, spock 1 and junit 4 extensions considered deprecated becuase they use - deprecated [dropwizard rule](https://www.dropwizard.io/en/release-2.0.x/manual/testing.html#junit-4). - -!!! tip - [Test framework-agnostic utilities](general.md) are useful with junit 5 or spock extensions in cases when - assertions required after application shutdown or to test application startup errors. - -Additionally, guicey provides several mechanisms at its core for application customization in tests (see below). - -## Setup objects - -Junit 5 extensions provide support for [setup objects](junit5.md#test-environment-setup): -a simple way to prepare test environment and apply context configuration (e.g. start test database). - -Spock 2 test could also use setup objects. - -## Configuration hooks - -Guicey provides [hooks mechanism](../hooks.md) to be able to modify -application configuration in tests. - -Using hooks you can disable installers, extensions, guicey bundles -or override guice bindings. - -It may also be useful to register additional extensions (e.g. to validate some internal behaviour). - -Example hook: - -```java -public class MyHook implements GuiceyConfigurationHook { - - public void configure(GuiceBundle.Builder builder) { - builder - .disableModules(FeatureXModule.class) - .disable(inPackage("com.foo.feature")) - .modulesOverride(new MockDaoModule()) - .option(Myoptions.DebugOption, true); - } -} -``` - -!!! note - You can modify [options](../options.md) in hook and so could enable some custom - debug/monitoring options specifically for test. - -There are special [spock](spock.md#hook-fields) and [junit](junit5.md#hook-fields) extensions for hooks registrations. - -## Disables - -You can use hooks to disable all not needed features in test: - -* [installers](../disables.md#disable-installers) -* [extensions](../disables.md#disable-extensions) -* [guice modules](../disables.md#disable-guice-modules) -* [guicey bundles](../disables.md#disable-bundles) -* [dropwizard bundles](../disables.md#disable-dropwizard-bundles) - -This way you can isolate (as much as possible) some feature for testing. - -The most helpful should be bundles disable (if you use bundles for features grouping) -and guice modules. - -Use [predicate disabling](../disables.md#disable-by-predicate). - -!!! note - It is supposed that disabling will be used instead of mocking - you simply remove what - you don't need and register replacements, if required. - -## Guice bindings override - -It is quite common requirement to override bindings for testing. For example, -you may want to mock database access. - -Guicey could use guice `Modules.override()` to help you override required bindings. -To use it prepare module only with changed bindings (bindings that must override existing). -For example, you want to replace ServiceX. You have few options: - -* If it implements interface, implement your own service and bind as -`bind(ServiceContract.class).to(MyServiceXImpl.class)` -* If service is a class, you can modify its behaviour with extended class -`bind(ServiceX.class).to(MyServiceXExt.class)` -* Or you can simply register some mock instance -`bind(ServiceX.class).toInstance(myMockInstance)` - -```java -public class MyOverridingModule extends AbstractModule { - - protected configure() { - bind(ServiceX.class).to(MyServiceXExt.class); - } -} -``` - -And register overriding module in hook: - -```java -public class MyHook implements GuiceyConfigurationHook { - public void configure(GuiceBundle.Builder builder) { - builder - .modulesOverride(new MyOverridingModule()); - } -} -``` - -### Debug bundles - -You can also use special guicey bundles, which modify application behaviour. -Bundles could contain additional listeners or services to gather additional metrics during -tests or validate behaviour. - -For example, guicey tests use bundle to enable restricted guice options like -`disableCircularProxies`. - -Bundles are also able to: - -* disable installers, extensions, gucie modules -* override guice bindings - -You can also use lookup mechanism to load bundles in tests. For example, -[system properties lookup](../bundles.md#system-property-lookup). - -## Overriding overridden beans - -Guicey provides [direct support for overriding guice bindings](../guice/override.md), -so in most cases you don't need to do anything. - -But, if you use this to override application bindings need to override such bindings in test (again), then you - may use provided custom [injector factory](../guice/injector.md#injector-factory): - -Register factory in guice bundle: - -```java -GuiceBundle.builder() - .injectorFactory(new BindingsOverrideInjectorFactory()) -``` - - -After that you can register overriding bindings (which will override even modules registered in `modulesOverride`) -with: - -```java -BindingsOverrideInjectorFactory.override(new MyOverridingModule()) -``` - -!!! important - It is assumed that overrding modules registration and application initialization - will be at the same thread (`ThreadLocal` used for holding registered modules to allow - parallel tests usage). - -For example, suppose we have some service `CustomerService` and it's implementation `CustomerServiceImpl`, -defined in some 3rd party module. For some reason we need to override this binding in the application: - -```java -public class OverridingModule extends AbstractModule { - @Override - protected void configure() { - bind(CustomerService.class).to(CustomCustomerServiceImpl.class); - } -} -``` - - -If we need to override this binding in test (again): - -(Simplified) registration looks like this: - -```java -GuiceBundle.builder() - .injectorFactory(new BindingsOverrideInjectorFactory()) - .modules(new ThirdPatyModule()) - // override binding for application needs - .modulesOverride(new OverridingModule()) - ... - .build() - -// register overriding somewhere -BindingsOverrideInjectorFactory.override(new TestOverridingModule()) -``` - -!!! tip - [Configuration hook](#configuration-hooks) may be used for static call (as a good integration point) - -After test startup, application will use customer service binding from TestOverridingModule. diff --git a/src/doc/docs/index.md b/src/doc/docs/index.md deleted file mode 100644 index 7a926481d..000000000 --- a/src/doc/docs/index.md +++ /dev/null @@ -1,57 +0,0 @@ -# Welcome to dropwizard-guicey - -!!! summary "" - [Guice](https://github.com/google/guice) `{{ gradle.guice }}` integration for [dropwizard](http://dropwizard.io) `{{ gradle.dropwizard }}`. - Compiled for `java 8`, compatible with `java 11 - 17`. - -**[Release Notes](about/release-notes.md)** - [History](about/history.md) - [Javadoc](https://javadoc.io/doc/ru.vyarus/dropwizard-guicey/) - [Support](about/support.md) - [License](about/license.md) - -!!! note "" - If you migrate from dropwizard 1.x then see [dropwizard upgrade instructions](https://www.dropwizard.io/en/release-2.0.x/manual/upgrade-notes/upgrade-notes-2_0_x.html) - and [guicey migration guide](http://xvik.github.io/dropwizard-guicey/5.0.0/about/release-notes/#migration-guide). - -## Main features - -* Auto configuration from [classpath scan](guide/scan.md) and [guice bindings](guide/guice/module-analysis.md#extensions-recognition). -* [Yaml config values bindings](guide/yaml-values.md) by path or unique sub objects. -* Advanced [Web support](guide/web.md) -* Dropwizard style [console reporting](guide/installers.md#reporting): detected (and installed) extensions are printed to console to remove uncertainty -* [Test support](guide/test/overview.md): custom junit and [spock](http://spockframework.org) extensions - - Advanced test abilities to [disable](guide/disables.md) or [override](guide/guice/override.md) application logic -* Developer friendly: - - core integrations [may be replaced](guide/disables.md#disable-installers) (to better fit needs) - - rich api for developing [custom integrations](guide/installers.md#writing-custom-installer), and hooking into [lifecycle](guide/events.md)) - - out of the box support for plug-n-play plugins ([auto discoverable](guide/bundles.md#service-loader-lookup)) - - [diagnostic tools](guide/diagnostic/diagnostic-tools.md) (reports), support for [custom diagnostic tools](guide/hooks.md#diagnostic) - -## Sponsors - -: [![Channel](img/sponsors/zoyi-ch.png)](https://channel.io "Channel") - - -If guicey makes your life easier, you can [support its development](https://www.patreon.com/guicey). - -## Documentation Summary - -### Introduction - -* [**Getting started**](getting-started.md) guide describes installation and provides core usage examples -* [**Concepts overview**](concepts.md) guide introduces core guicey concepts and demonstrates differences from pure dropwizard usage -* [**Guice**](guice.md) the essence of guice integration -* [**Testing**](tests.md) describes integration testing techniques -* [**Decomposition**](decomposition.md) guide on writing re-usable modules - -### Reference -* [**User guide**](guide/configuration.md) contains detailed feature descriptions. It is good to read, but it also functions - well as a reference if you're short on time. -* [**Installers**](installers/resource.md) describes all guicey installers. Use it as a *extensions hand book*. -* [**Modules**](guide/modules.md) external extension modules overview. -* [**Examples**](examples/authentication.md) important usage examples. - -## Sources structure - -* [Guicey repository]((https://github.com/xvik/dropwizard-guicey)): guicey itself and (these) docs -* [Modules repository](https://github.com/xvik/dropwizard-guicey-ext): extension [modules](guide/modules.md) (integrations) -are maintained in the separate repository -* [Examples repository](https://github.com/xvik/dropwizard-guicey-examples): holds code samples for main features dropwizard -bundles and extension modules. diff --git a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java b/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java deleted file mode 100644 index 676b3de51..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/debug/report/guice/util/visitor/GuiceScopingVisitor.java +++ /dev/null @@ -1,63 +0,0 @@ -package ru.vyarus.dropwizard.guice.debug.report.guice.util.visitor; - -import com.google.inject.Scope; -import com.google.inject.Scopes; -import com.google.inject.Singleton; -import com.google.inject.servlet.RequestScoped; -import com.google.inject.servlet.ServletScopes; -import com.google.inject.servlet.SessionScoped; -import com.google.inject.spi.DefaultBindingScopingVisitor; -import ru.vyarus.dropwizard.guice.module.installer.feature.eager.EagerSingleton; -import ru.vyarus.dropwizard.guice.module.support.scope.Prototype; - -import java.lang.annotation.Annotation; - -/** - * Guice binding scope analyzer. Does not support custom scopes. Works correctly only on bindings from injector - * (for module element only manually declared scopes are visible and not annotations). - */ -public class GuiceScopingVisitor - extends DefaultBindingScopingVisitor> { - - @Override - public Class visitEagerSingleton() { - return EagerSingleton.class; - } - - @Override - public Class visitScope(final Scope scope) { - Class res = null; - if (Scopes.SINGLETON.equals(scope)) { - res = javax.inject.Singleton.class; - } - if (Scopes.NO_SCOPE.equals(scope)) { - res = Prototype.class; - } - if (ServletScopes.REQUEST.equals(scope)) { - res = RequestScoped.class; - } - if (ServletScopes.SESSION.equals(scope)) { - res = SessionScoped.class; - } - // not supporting custom scopes - return res; - } - - @Override - @SuppressWarnings("unchecked") - public Class visitScopeAnnotation(final Class scopeAnnotation) { - // always return javax.inject annotation to simplify checks - if (scopeAnnotation.equals(Singleton.class)) { - return javax.inject.Singleton.class; - } - return scopeAnnotation; - } - - @Override - public Class visitNoScoping() { - // special case: when checking direct module elements guice know only directly configured scope info and - // ignore annotations.. so instead of correct scope from annotation no scope is returned - - return Prototype.class; - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java b/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java deleted file mode 100644 index af51b21da..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/bundle/GuiceyBootstrap.java +++ /dev/null @@ -1,334 +0,0 @@ -package ru.vyarus.dropwizard.guice.module.installer.bundle; - -import com.google.common.base.Preconditions; -import com.google.inject.Module; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.ConfiguredBundle; -import io.dropwizard.setup.Bootstrap; -import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; -import ru.vyarus.dropwizard.guice.module.context.option.Option; -import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; -import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; - -import java.util.List; -import java.util.Optional; -import java.util.function.Supplier; - -/** - * Guicey initialization object. Provides almost the same configuration methods as - * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder}. Also, contains dropwizard bootstrap objects. - * May register pure dropwizard bundles. - *

        - * In contrast to main builder, guicey bundle can't: - *

          - *
        • Disable bundles (because at this stage bundles already partly processed)
        • - *
        • Use generic disable predicates (to not allow bundles disable, moreover it's tests-oriented feature)
        • - *
        • Change options (because some bundles may already apply configuration based on changed option value - * which will mean inconsistent state)
        • - *
        • Register listener, implementing {@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook} - * (because it's too late - all hooks were processed)
        • - *
        • Register some special objects like custom injector factory or custom bundles lookup
        • - *
        - * - * @author Vyacheslav Rusakov - * @since 01.08.2015 - */ -@SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") -public class GuiceyBootstrap { - - private final ConfigurationContext context; - private final List iterationBundles; - - public GuiceyBootstrap(final ConfigurationContext context, final List iterationBundles) { - this.context = context; - this.iterationBundles = iterationBundles; - } - - /** - * Note: application is already in run phase, so it's too late to configure dropwizard bootstrap object. Object - * provided just for consultation. - * - * @param configuration type - * @return dropwizard bootstrap instance - */ - @SuppressWarnings("unchecked") - public Bootstrap bootstrap() { - return context.getBootstrap(); - } - - /** - * Application instance may be useful for complex (half manual) integrations where access for - * injector is required. - * For example, manually registered - * {@link io.dropwizard.lifecycle.Managed} may access injector in it's start method by calling - * {@link ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup#getInjector(Application)}. - *

        - * NOTE: it will work in this example, because injector access will be after injector creation. - * Directly inside bundle initialization method injector could not be obtained as it's not exists yet. - * - * @return dropwizard application instance - */ - public Application application() { - return context.getBootstrap().getApplication(); - } - - /** - * Read option value. Options could be set only in application root - * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(Enum, Object)}. - * If value wasn't set there then default value will be returned. Null may return only if it was default value - * and no new value were assigned. - *

        - * Option access is tracked as option usage (all tracked data is available through - * {@link ru.vyarus.dropwizard.guice.module.context.option.OptionsInfo}). - * - * @param option option enum - * @param option value type - * @param helper type to define option - * @return assigned option value or default value - * @see Option more options info - * @see ru.vyarus.dropwizard.guice.GuiceyOptions options example - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#option(java.lang.Enum, java.lang.Object) - * options definition - */ - public V option(final T option) { - return context.option(option); - } - - /** - * Register guice modules. - *

        - * Note that this registration appear under initialization phase and so neither configuration nor environment - * objects are not available yet. If you need them for module, then you can wrap it with - * {@link ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule} or register modules in run phase - * (inside {@link GuiceyBundle#run(GuiceyEnvironment)}). - * - * @param modules one or more guice modules - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modules(com.google.inject.Module...) - * @see ru.vyarus.dropwizard.guice.module.support.DropwizardAwareModule - */ - public GuiceyBootstrap modules(final Module... modules) { - Preconditions.checkState(modules.length > 0, "Specify at least one module"); - context.registerModules(modules); - return this; - } - - /** - * Override modules (using guice {@link com.google.inject.util.Modules#override(Module...)}). - * - * @param modules overriding modules - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#modulesOverride(Module...) - */ - public GuiceyBootstrap modulesOverride(final Module... modules) { - context.registerModulesOverride(modules); - return this; - } - - /** - * If bundle provides new installers then they must be declared here. - * Optionally, core or other 3rd party installers may be declared also to indicate dependency - * (duplicate installers registrations will be removed). - * - * @param installers feature installer classes to register - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#installers(Class[]) - */ - @SafeVarargs - public final GuiceyBootstrap installers(final Class... installers) { - context.registerInstallers(installers); - return this; - } - - /** - * Bundle should not rely on auto-scan mechanism and so must declare all extensions manually - * (this better declares bundle content and speed ups startup). - *

        - * NOTE: startup will fail if bean not recognized by installers. Use {@link #extensionsOptional(Class[])} to - * register optional extension. - *

        - * Alternatively, you can manually bind extensions in guice module and they would be recognized - * ({@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}). - * - * @param extensionClasses extension bean classes to register - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#extensions(Class[]) - */ - public GuiceyBootstrap extensions(final Class... extensionClasses) { - context.registerExtensions(extensionClasses); - return this; - } - - /** - * The same as {@link #extensions(Class[])}, but, in case if no installer recognize extension, will be - * automatically disabled instead of throwing error. Useful for optional extensions declaration in 3rd party - * bundles (where it is impossible to be sure what other bundles will be used and so what installers will - * be available). - *

        - * Alternatively, you can manually bind extensions in guice module and they would be recognized - * ({@link ru.vyarus.dropwizard.guice.GuiceyOptions#AnalyzeGuiceModules}). Extensions with no available target - * installer will simply wouldn't be detected (because installers used for recognition) and so there is no need - * to mark them as optional in this case. - * - * @param extensionClasses extension bean classes to register - * @return bootstrap instance for chained calls - */ - public GuiceyBootstrap extensionsOptional(final Class... extensionClasses) { - context.registerExtensionsOptional(extensionClasses); - return this; - } - - /** - * Register other guicey bundles for installation. - *

        - * Equal instances of the same type will be considered as duplicate. - * - * @param bundles guicey bundles - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#bundles(GuiceyBundle...) - */ - public GuiceyBootstrap bundles(final GuiceyBundle... bundles) { - // remember only non duplicate bundles - iterationBundles.addAll(context.registerBundles(bundles)); - return this; - } - - /** - * Shortcut for dropwizard bundles registration (instead of {@code bootstrap().addBundle()}), but with - * duplicates detection and tracking in diagnostic reporting. Dropwizard bundle is immediately initialized. - * - * @param bundles dropwizard bundles to register - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#dropwizardBundles(ConfiguredBundle...) - */ - public GuiceyBootstrap dropwizardBundles(final ConfiguredBundle... bundles) { - context.registerDropwizardBundles(bundles); - return this; - } - - /** - * @param installers feature installer types to disable - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableInstallers(Class[]) - */ - @SafeVarargs - public final GuiceyBootstrap disableInstallers(final Class... installers) { - context.disableInstallers(installers); - return this; - } - - /** - * @param extensions extensions to disable (manually added, registered by bundles or with classpath scan) - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableExtensions(Class[]) - */ - public final GuiceyBootstrap disableExtensions(final Class... extensions) { - context.disableExtensions(extensions); - return this; - } - - /** - * Disable both usual and overriding guice modules. - *

        - * If bindings analysis is not disabled, could also disable inner (transitive) modules, but only inside - * normal modules. - * - * @param modules guice module types to disable - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableModules(Class[]) - */ - @SafeVarargs - public final GuiceyBootstrap disableModules(final Class... modules) { - context.disableModules(modules); - return this; - } - - /** - * Guicey broadcast a lot of events in order to indicate lifecycle phases - * ({@linkplain ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle}). Listener, registered in bundles - * could listen events from {@link ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle#BundlesInitialized}. - *

        - * Listener is not registered if equal listener was already registered ({@link java.util.Set} used as - * listeners storage), so if you need to be sure that only one instance of some listener will be used - * implement {@link Object#equals(Object)}. - * - * @param listeners guicey lifecycle listeners - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#listen(GuiceyLifecycleListener...) - */ - public GuiceyBootstrap listen(final GuiceyLifecycleListener... listeners) { - context.lifecycle().register(listeners); - return this; - } - - /** - * Share global state to be used in other bundles (during configuration). This was added for very special cases - * when shared state is unavoidable (to not re-invent the wheel each time)! - *

        - * During application strartup, shared state could be requested with a static call - * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#getStartupInstance()}, but only - * from main thread. - *

        - * Internally, state is linked to application instance, so it would be safe to use with concurrent tests. - * Value could be accessed statically with application instance: - * {@link ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState#lookup(Application, Class)}. - *

        - * In some cases, it is preferred to use bundle class as key. Value could be set only once - * (to prevent hard to track situations). - *

        - * If initialization point could vary (first access should initialize it) use {@link #sharedState(Class, Supplier)} - * instead. - * - * @param key shared object key - * @param value shared object - * @return bootstrap instance for chained calls - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState - */ - public GuiceyBootstrap shareState(final Class key, final Object value) { - context.getSharedState().put(key, value); - return this; - } - - /** - * Alternative shared value initialization for cases when first accessed bundle should init state value - * and all other just use it. - * - * @param key shared object key - * @param defaultValue default object provider - * @param shared object type - * @return shared object (possibly just created) - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState - */ - public T sharedState(final Class key, final Supplier defaultValue) { - return context.getSharedState().get(key, defaultValue); - } - - /** - * Access shared value. - * - * @param key shared object key - * @param shared object type - * @return shared object - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState - */ - public Optional sharedState(final Class key) { - return Optional.ofNullable(context.getSharedState().get(key)); - } - - /** - * Used to access shared state value and immediately fail if value not yet set (most likely due to incorrect - * configuration order). - * - * @param key shared object key - * @param message exception message (could use {@link String#format(String, Object...)} placeholders) - * @param args placeholder arguments for error message - * @param shared object type - * @return shared object - * @throws IllegalStateException if no value available - * @see ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState - */ - public T sharedStateOrFail(final Class key, final String message, final Object... args) { - return context.getSharedState().getOrFail(key, message, args); - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java b/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java deleted file mode 100644 index 777202146..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/module/installer/internal/ModulesSupport.java +++ /dev/null @@ -1,311 +0,0 @@ -package ru.vyarus.dropwizard.guice.module.installer.internal; - -import com.google.common.base.Stopwatch; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; -import com.google.inject.Binding; -import com.google.inject.Key; -import com.google.inject.Module; -import com.google.inject.spi.Element; -import com.google.inject.spi.Elements; -import com.google.inject.spi.LinkedKeyBinding; -import com.google.inject.util.Modules; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import ru.vyarus.dropwizard.guice.GuiceyOptions; -import ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule; -import ru.vyarus.dropwizard.guice.module.context.ConfigurationContext; -import ru.vyarus.dropwizard.guice.module.context.option.Options; -import ru.vyarus.dropwizard.guice.module.context.stat.Stat; -import ru.vyarus.dropwizard.guice.module.installer.util.BindingUtils; -import ru.vyarus.dropwizard.guice.module.support.*; - -import java.util.*; -import java.util.stream.Collectors; - -import static ru.vyarus.dropwizard.guice.GuiceyOptions.AnalyzeGuiceModules; -import static ru.vyarus.dropwizard.guice.GuiceyOptions.InjectorStage; -import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.InstallersTime; -import static ru.vyarus.dropwizard.guice.module.context.stat.Stat.ModulesProcessingTime; - -/** - * Helper class for guice modules processing. - * - * @author Vyacheslav Rusakov - * @since 25.04.2018 - */ -public final class ModulesSupport { - - private static final Logger LOGGER = LoggerFactory.getLogger(ModulesSupport.class); - - private ModulesSupport() { - } - - /** - * Post-process registered modules by injecting bootstrap, configuration, environment and options objects. - * - * @param context configuration context - */ - @SuppressWarnings("unchecked") - public static void configureModules(final ConfigurationContext context) { - final Options options = new Options(context.options()); - for (Module mod : context.getEnabledModules()) { - if (mod instanceof BootstrapAwareModule) { - ((BootstrapAwareModule) mod).setBootstrap(context.getBootstrap()); - } - if (mod instanceof ConfigurationAwareModule) { - ((ConfigurationAwareModule) mod).setConfiguration(context.getConfiguration()); - } - if (mod instanceof ConfigurationTreeAwareModule) { - ((ConfigurationTreeAwareModule) mod).setConfigurationTree(context.getConfigurationTree()); - } - if (mod instanceof EnvironmentAwareModule) { - ((EnvironmentAwareModule) mod).setEnvironment(context.getEnvironment()); - } - if (mod instanceof OptionsAwareModule) { - ((OptionsAwareModule) mod).setOptions(options); - } - } - } - - /** - * Prepares modules to use for injector creation (applies module overrides). - * - * @param context configuration context - * @return modules for injector creation - */ - public static Iterable prepareModules(final ConfigurationContext context) { - final Stopwatch timer = context.stat().timer(ModulesProcessingTime); - final List overridingModules = context.getOverridingModules(); - // repackage normal modules to reveal all guice extensions - final List normalModules = analyzeModules(context, timer); - - final Iterable res = overridingModules.isEmpty() ? normalModules - : Collections.singletonList(Modules.override(normalModules).with(overridingModules)); - timer.stop(); - return res; - } - - /** - * Search for extensions in guice bindings (directly declared in modules). - * Only user provided modules are analyzed. Overriding modules are not analyzed. - *

        - * Use guice SPI. In order to avoid duplicate analysis in injector creation time, wrap - * parsed elements as new module (and use it instead of original modules). Also, if - * bound extension is disabled, target binding is simply removed (in order to - * provide the same disable semantic as with usual extensions). - * - * @param context configuration context - * @return list of repackaged modules to use - */ - private static List analyzeModules(final ConfigurationContext context, - final Stopwatch modulesTimer) { - List modules = context.getNormalModules(); - final Boolean configureFromGuice = context.option(AnalyzeGuiceModules); - // one module mean no user modules registered - if (modules.size() > 1 && configureFromGuice) { - // analyzing only user bindings (excluding overrides and guicey technical bindings) - final GuiceBootstrapModule bootstrap = (GuiceBootstrapModule) modules.remove(modules.size() - 1); - try { - // find extensions and remove bindings if required (disabled extensions) - final Stopwatch gtime = context.stat().timer(Stat.BindingsResolutionTime); - final List elements = new ArrayList<>( - Elements.getElements(context.option(InjectorStage), modules)); - gtime.stop(); - - // exclude analysis time from modules processing time (it's installer time) - modulesTimer.stop(); - analyzeAndFilterBindings(context, modules, elements); - modulesTimer.start(); - - // wrap raw elements into module to avoid duplicate work on guice startup and put back bootstrap - modules = Arrays.asList(Elements.getModule(elements), bootstrap); - } catch (Exception ex) { - // better show meaningful message then just fail entire startup with ambiguous message - // NOTE if guice configuration is not OK it will fail here too, but user will see injector creation - // error as last error in logs. - LOGGER.error("Failed to analyze guice bindings - skipping this step. Note that configuration" - + " from bindings may be switched off with " + GuiceyOptions.class.getSimpleName() + "." - + AnalyzeGuiceModules.name() + " option.", ex); - // recover and use original modules - modules.add(bootstrap); - if (!modulesTimer.isRunning()) { - modulesTimer.start(); - } - } - } - return modules; - } - - @SuppressWarnings("PMD.NcssCount") - private static void analyzeAndFilterBindings(final ConfigurationContext context, - final List analyzedModules, - final List elements) { - final Stopwatch itimer = context.stat().timer(InstallersTime); - final Stopwatch timer = context.stat().timer(Stat.ExtensionsRecognitionTime); - context.stat().count(Stat.BindingsCount, elements.size()); - final List disabledModules = prepareDisabledModules(context); - final Set actuallyDisabledModules = new HashSet<>(); - final List removedBindings = new ArrayList<>(); - final Iterator it = elements.iterator(); - final List> extensions = new ArrayList<>(); - // extension may be recognized by linked key and linked keys may need to be removed too - // right key -> binding - final Multimap linkedBindings = LinkedHashMultimap.create(); - while (it.hasNext()) { - final Element element = it.next(); - if (isInDisabledModule(element, disabledModules, actuallyDisabledModules)) { - // remove all bindings under disabled modules - it.remove(); - context.stat().count(Stat.RemovedBindingsCount, 1); - continue; - } - // filter constants, listeners, aop etc. - if (element instanceof Binding - && checkBindingRemoveRequired(context, (Binding) element, extensions, linkedBindings)) { - it.remove(); - removedBindings.add((Binding) element); - } - } - // recognize extensions in linked bindings and remove required bindings - for (Binding binding : findLinkedBindingsToRemove(context, extensions, linkedBindings)) { - elements.remove(binding); - removedBindings.add(binding); - } - if (!actuallyDisabledModules.isEmpty()) { - LOGGER.debug("Removed inner guice modules: {}", actuallyDisabledModules); - } - context.stat().count(Stat.RemovedInnerModules, actuallyDisabledModules.size()); - context.stat().count(Stat.RemovedBindingsCount, removedBindings.size()); - context.lifecycle().modulesAnalyzed(analyzedModules, extensions, toModuleClasses(actuallyDisabledModules), - removedBindings); - timer.stop(); - itimer.stop(); - } - - private static boolean checkBindingRemoveRequired(final ConfigurationContext context, - final Binding binding, - final List> extensions, - final Multimap linkedBindings) { - final Key key = binding.getKey(); - if (isPossibleExtension(key)) { - context.stat().count(Stat.AnalyzedBindingsCount, 1); - final Class type = key.getTypeLiteral().getRawType(); - if (ExtensionsSupport.registerExtensionBinding(context, type, - binding, BindingUtils.getTopDeclarationModule(binding))) { - LOGGER.debug("Extension detected from guice binding: {}", type.getSimpleName()); - extensions.add(type); - return !context.isExtensionEnabled(type); - } - } - // note if linked binding recognized as extension by its key - it would not be counted (not needed) - if (binding instanceof LinkedKeyBinding) { - // remember all linked bindings (do not recognize on first path to avoid linked binding check before - // real binding) - final LinkedKeyBinding linkedBind = (LinkedKeyBinding) binding; - linkedBindings.put(linkedBind.getLinkedKey(), linkedBind); - } - return false; - } - - // extension bindings may be only unqualified, class only (no generified types) - private static boolean isPossibleExtension(final Key key) { - return key.getAnnotation() == null && key.getTypeLiteral().getType() instanceof Class; - } - - // links map is: linked type (end) -> binding - private static List findLinkedBindingsToRemove(final ConfigurationContext context, - final List> extensions, - final Multimap links) { - // try to recognize extensions in links - for (Map.Entry entry : links.entries()) { - final Key key = entry.getKey(); - final Class type = key.getTypeLiteral().getRawType(); - final LinkedKeyBinding binding = entry.getValue(); - if (!isPossibleExtension(key)) { - continue; - } - // try to detect extension in linked type (binding already analyzed so no need to count) - if (!extensions.contains(type) && ExtensionsSupport.registerExtensionBinding(context, type, - binding, BindingUtils.getTopDeclarationModule(binding))) { - LOGGER.debug("Extension detected from guice link binding: {}", type.getSimpleName()); - extensions.add(type); - } - } - // find disabled bindings (already removed) - // for extensions recognized from links above imagine as we removed some (not existing) bindings - final List removedExtensions = extensions.stream() - .filter(it -> !context.isExtensionEnabled(it)) - .map(Key::get) - .collect(Collectors.toList()); - return removeChains(removedExtensions, links); - } - - /** - * Pass in removed binding keys. Need to find all links ending on removed type and remove. - * Next, repeat with just removed types (to clean up entire chains because with it context may not start). - * For example: {@code bind(Interface1).to(Interface2); bind(Interface2).to(Extension)} - * Extension detected as extension, but if its disabled then link (Interface2 -> Extension) must be removed - * but without it Interface1 -> Interface2 remains and fail context becuase its just interfaces - * that's why entire chains must be removed. - * - * @param removed removed keys (to clean links leading to this keys) - * @param bindings all linked bindings (actually without links with recognized extension in left part) - * @return list of bindings to remove - */ - private static List removeChains(final List removed, - final Multimap bindings) { - final List newlyRemoved = new ArrayList<>(); - final List res = new ArrayList<>(); - - for (Key removedKey : removed) { - // remove all links ending on removed key - for (LinkedKeyBinding bnd : bindings.get(removedKey)) { - res.add(bnd); - newlyRemoved.add(bnd.getKey()); - } - } - - // continue removing chains - if (!newlyRemoved.isEmpty()) { - res.addAll(removeChains(newlyRemoved, bindings)); - } - return res; - } - - private static List prepareDisabledModules(final ConfigurationContext context) { - final List res = new ArrayList<>(); - for (Class cls : context.getDisabledModuleTypes()) { - res.add(cls.getName()); - } - return res; - } - - private static boolean isInDisabledModule(final Element element, - final List disabled, - final Set actuallyDisabled) { - if (!disabled.isEmpty()) { - final List modules = BindingUtils.getModules(element); - // need to check from top modules to lower, otherwise removed modules list will be incorrect - for (int i = modules.size() - 1; i >= 0; i--) { - final String mod = modules.get(i); - if (disabled.contains(mod)) { - actuallyDisabled.add(mod); - return true; - } - } - } - return false; - } - - private static List> toModuleClasses(final Set modules) { - if (modules.isEmpty()) { - return Collections.emptyList(); - } - final List> res = new ArrayList<>(); - for (String mod : modules) { - res.add(BindingUtils.getModuleClass(mod)); - } - return res; - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java b/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java deleted file mode 100644 index 34a42972d..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/ClientSupport.java +++ /dev/null @@ -1,231 +0,0 @@ -package ru.vyarus.dropwizard.guice.test; - -import com.google.common.base.Preconditions; -import io.dropwizard.jersey.jackson.JacksonFeature; -import io.dropwizard.setup.Environment; -import io.dropwizard.testing.DropwizardTestSupport; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.HttpUrlConnectorProvider; -import org.glassfish.jersey.client.JerseyClient; -import org.glassfish.jersey.client.JerseyClientBuilder; -import ru.vyarus.dropwizard.guice.module.installer.util.PathUtils; - -import javax.ws.rs.client.WebTarget; - -/** - * {@link JerseyClient} support for direct web tests (complete dropwizard startup). - *

        - * Client support maintains single {@link JerseyClient} instance. It may be used for calling any urls (not just - * application). Class provides many utility methods for automatic construction of base context paths, so - * tests could be completely independent from actual configuration. - * - * @author Vyacheslav Rusakov - * @since 04.05.2020 - */ -public class ClientSupport implements AutoCloseable { - private static final String HTTP_LOCALHOST = "http://localhost:"; - - private final DropwizardTestSupport support; - private JerseyClient client; - - public ClientSupport(final DropwizardTestSupport support) { - this.support = support; - } - - /** - * Single client instance maintained within test and method will always return the same instance. - * - * @return client instance - */ - public JerseyClient getClient() { - synchronized (this) { - if (client == null) { - client = clientBuilder().build(); - } - return client; - } - } - - /** - * @return main context port - * @throws NullPointerException for guicey test - */ - public int getPort() { - return support.getLocalPort(); - } - - /** - * @return admin context port - * @throws NullPointerException for guicey test - */ - public int getAdminPort() { - return support.getAdminPort(); - } - - /** - * For example, with default configuration it would be "http://localhost:8080/". If "server.applicationContextPath" - * would be changed to "/someth" then method will return "http://localhost:8080/someth/". - *

        - * Returned path will always end with slash. - * - * @return base path for application main context - * @throws NullPointerException for guicey test - */ - public String basePathMain() { - final String contextMapping = support.getEnvironment().getApplicationContext().getContextPath(); - return PathUtils.trailingSlash( - PathUtils.path(HTTP_LOCALHOST + getPort(), contextMapping)); - } - - /** - * For example, with default configuration it would be "http://localhost:8080/". If "server.rootPath" - * would be changed to "/someth" then method will return "http://localhost:8080/someth/". - * If main context mapping changed from root, then returned path will count in too - * (e.g. "http://localhost:8080/root/rest/", when "server.applicationContextPath" is "/root"). - *

        - * Returned path will always end with slash. - * - * @return base path for admin context - * @throws NullPointerException for guicey test - */ - public String basePathAdmin() { - final String contextMapping = support.getEnvironment().getAdminContext().getContextPath(); - return PathUtils.trailingSlash( - PathUtils.path(HTTP_LOCALHOST + getAdminPort(), contextMapping)); - } - - /** - * For example, with default configuration it would be "http://localhost:8080/". If "server.applicationContextPath" - * would be changed to "/someth" then method will return "http://localhost:8080/someth/". - *

        - * Returned path will always end with slash. - * - * @return base path for rest - * @throws NullPointerException for guicey test - */ - public String basePathRest() { - final Environment env = support.getEnvironment(); - final String contextPath = env.getJerseyServletContainer() - .getServletConfig().getServletContext().getContextPath(); - // server.rootPath - final String restMapping = PathUtils.trailingSlash(PathUtils.trimStars(env.jersey().getUrlPattern())); - return PathUtils.trailingSlash( - PathUtils.path(HTTP_LOCALHOST + getPort(), contextPath, restMapping)); - } - - /** - * Unbounded (universal) {@link WebTarget} construction shortcut. First url part must contain host (port) target. - * When multiple parameters provided, they are connected with "/", avoiding duplicate slash appearances - * so, for example, "app, path", "app/, /path" or any other variation would always lead to correct "app/path"). - * Essentially this is the same as using {@link WebTarget#path(String)} multiple times (after initial target - * creation). - *

        - * Example: {@code .target("http://localhotst:8080/smth/").request().buildGet().invoke()} - *

        - * NOTE: safe to use with guicey-only tests (when web part not started) to call any external url. - * - * @param paths one or more path parts (joined with '/') - * @return jersey web target object - */ - public WebTarget target(final String... paths) { - Preconditions.checkState(paths.length != 0, - "Target required (e.g. http://localhost:8080/)"); - return getClient().target(PathUtils.path(paths)); - } - - /** - * Shortcut for {@link WebTarget} creation for main context path. Method abstracts you from actual configuration - * so you can just call servlets by their registration uri. - *

        - * Without parameters it will target main context root: {@code .targetMain().request().buildGet().invoke()} would - * call "http://localhost:8080/". - *

        - * Additional paths may be provided to construct urls: - * {@code .targetMain("something").request().buildGet().invoke()} would call "http://localhost:8080/something" - * and {@code .targetMain("foo", "bar").request().buildGet().invoke()} would call "http://localhost:8080/foo/bar". - * Last example is equivalent to jersey api (kind of shortcut): - * {@code .targetMain().path("foo").path("bar").request().buildGet().invoke()}. - * - * @param paths zero, one or more path parts (joined with '/') and appended to base path - * @return jersey web target object for main context - * @see #basePathMain() for base use construction details - * @throws NullPointerException for guicey test - */ - public WebTarget targetMain(final String... paths) { - return target(merge(basePathMain(), paths)); - } - - /** - * Shortcut for {@link WebTarget} creation for admin context path. Method abstracts you from actual configuration - * so you can just call servlets by their registration uri. - *

        - * Without parameters it will target admin context root: {@code .targetAdmin().request().buildGet().invoke()} would - * call "http://localhost:8081/". For simple server it would be "http://localhost:8080/admin/". - *

        - * Additional paths may be provided to construct urls: - * {@code .targetAdmin("something").request().buildGet().invoke()} would call "http://localhost:8081/something" - * and {@code .targetAdmin("foo", "bar").request().buildGet().invoke()} would call "http://localhost:8081/foo/bar". - * Last example is equivalent to jersey api (kind of shortcut): - * {@code .targetAdmin().path("foo").path("bar").request().buildGet().invoke()}. - * - * @param paths zero, one or more path parts (joined with '/') and appended to base path - * @return jersey web target object for admin context - * @see #basePathAdmin() for base use construction details - * @throws NullPointerException for guicey test - */ - public WebTarget targetAdmin(final String... paths) { - return target(merge(basePathAdmin(), paths)); - } - - /** - * Shortcut for {@link WebTarget} creation for rest context path. Method abstracts you from actual configuration - * so you can just call rest resources by their registration uri. - *

        - * Without parameters it will target rest context root: {@code .targetRest().request().buildGet().invoke()} would - * call "http://localhost:8080/". - *

        - * Additional paths may be provided to construct urls: - * {@code .targetRest("something").request().buildGet().invoke()} would call "http://localhost:8080/something" - * and {@code .targetRest("foo", "bar").request().buildGet().invoke()} would call "http://localhost:8080/foo/bar". - * Last example is equivalent to jersey api (kind of shortcut): - * {@code .targetRest().path("foo").path("bar").request().buildGet().invoke()}. - * - * @param paths zero, one or more path parts (joined with '/') and appended to base path - * @return jersey web target object for rest context - * @see #basePathRest() for base use construction details - * @throws NullPointerException for guicey test - */ - public WebTarget targetRest(final String... paths) { - return target(merge(basePathRest(), paths)); - } - - @Override - public void close() throws Exception { - synchronized (this) { - if (client != null) { - client.close(); - client = null; - } - } - } - - private JerseyClientBuilder clientBuilder() { - return new JerseyClientBuilder() - .register(new JacksonFeature(support.getEnvironment().getObjectMapper())) - .property(ClientProperties.CONNECT_TIMEOUT, 1000) - .property(ClientProperties.READ_TIMEOUT, 5000) - .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); - } - - private String[] merge(final String base, final String... addition) { - final String[] res; - if (addition.length == 0) { - res = new String[]{base}; - } else { - res = new String[addition.length + 1]; - res[0] = base; - System.arraycopy(addition, 0, res, 1, addition.length); - } - return res; - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java b/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java deleted file mode 100644 index 9f58f09ab..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/GuiceyTestSupport.java +++ /dev/null @@ -1,122 +0,0 @@ -package ru.vyarus.dropwizard.guice.test; - -import com.google.common.base.Preconditions; -import com.google.inject.Key; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.cli.Command; -import io.dropwizard.configuration.ConfigurationSourceProvider; -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.DropwizardTestSupport; - -import javax.annotation.Nullable; -import java.util.function.Function; - - -/** - * An alternative to {@link DropwizardTestSupport} which does not run jetty (web part) allowing to test only guice - * context. Internally, {@link TestCommand} used instead of {@link io.dropwizard.cli.ServerCommand}. - *

        - * Supposed to be used in cases when application startup fail must be tested: - * {@code new GuiceyTestSupport(MyApp.class, (String) null).before()}. - * - * @param configuration type - * @author Vyacheslav Rusakov - * @since 03.02.2022 - */ -public class GuiceyTestSupport extends DropwizardTestSupport { - - private static final ThreadLocal COMMAND = new ThreadLocal<>(); - - public GuiceyTestSupport(final Class> applicationClass, - final @Nullable String configPath, - final ConfigOverride... configOverrides) { - this(applicationClass, configPath, (String) null, configOverrides); - } - - public GuiceyTestSupport(final Class> applicationClass, - final @Nullable String configPath, - final @Nullable ConfigurationSourceProvider configSourceProvider, - final ConfigOverride... configOverrides) { - this(applicationClass, configPath, configSourceProvider, null, configOverrides); - } - - public GuiceyTestSupport(final Class> applicationClass, - final @Nullable String configPath, - final @Nullable ConfigurationSourceProvider configSourceProvider, - final @Nullable String customPropertyPrefix, - final ConfigOverride... configOverrides) { - super(applicationClass, configPath, configSourceProvider, customPropertyPrefix, - new CmdProvider<>(), configOverrides); - } - - public GuiceyTestSupport(final Class> applicationClass, - final @Nullable String configPath, - final @Nullable String customPropertyPrefix, - final ConfigOverride... configOverrides) { - super(applicationClass, configPath, customPropertyPrefix, new CmdProvider<>(), configOverrides); - } - - public GuiceyTestSupport(final Class> applicationClass, - final C configuration) { - super(applicationClass, configuration, new CmdProvider<>()); - } - - /** - * Normally, {@link #before()} and {@link #after()} methods are called separately. This method is a shortcut - * mostly for errors testing when {@link #before()} assumed to fail to make sure {@link #after()} will be called - * in any case: {@code testSupport.run(null)}. - * - * @param callback callback (may be null) - * @param result type - * @return callback result - * @throws Exception any appeared exception - */ - public T run(final @Nullable TestSupport.RunCallback callback) throws Exception { - return TestSupport.run(this, callback); - } - - /** - * Shortcut for accessing guice beans. - * - * @param type target bean type - * @param bean type - * @return bean instance - */ - public T getBean(final Class type) { - return TestSupport.getBean(this, type); - } - - /** - * Shortcut for accessing guice beans. - * - * @param key binding key - * @param bean type - * @return bean instance - */ - public T getBean(final Key key) { - return TestSupport.getBean(this, key); - } - - @Override - public void after() { - super.after(); - final TestCommand cmd = COMMAND.get(); - if (cmd != null) { - COMMAND.remove(); - cmd.stop(); - } - } - - static class CmdProvider implements Function, Command> { - - @Override - public Command apply(final Application application) { - Preconditions.checkState(GuiceyTestSupport.COMMAND.get() == null, - "Command already bound in thread"); - final TestCommand cmd = new TestCommand<>(application); - GuiceyTestSupport.COMMAND.set(cmd); - return cmd; - } - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java b/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java deleted file mode 100644 index 52d3f674e..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/TestCommand.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.vyarus.dropwizard.guice.test; - -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.cli.EnvironmentCommand; -import io.dropwizard.setup.Environment; -import net.sourceforge.argparse4j.inf.Namespace; -import org.eclipse.jetty.util.component.ContainerLifeCycle; - -/** - * Lightweight variation of server command for testing purposes. - * Handles managed objects lifecycle. - * - * @param configuration type - * @author Vyacheslav Rusakov - * @since 23.10.2014 - */ -public class TestCommand extends EnvironmentCommand { - - private final Class configurationClass; - private ContainerLifeCycle container; - - public TestCommand(final Application application) { - super(application, "guicey-test", "Specific command to run guice context without jetty server"); - cleanupAsynchronously(); - configurationClass = application.getConfigurationClass(); - } - - @Override - protected void run(final Environment environment, final Namespace namespace, - final C configuration) throws Exception { - // simulating managed objects lifecycle support - container = new ContainerLifeCycle(); - environment.lifecycle().attach(container); - container.start(); - } - - public void stop() { - if (container != null) { - try { - container.stop(); - } catch (Exception e) { - throw new IllegalStateException("Failed to stop managed objects container", e); - } - container.destroy(); - } - cleanup(); - } - - @Override - protected Class getConfigurationClass() { - return configurationClass; - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java b/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java deleted file mode 100644 index 61a6ced8b..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/TestSupport.java +++ /dev/null @@ -1,241 +0,0 @@ -package ru.vyarus.dropwizard.guice.test; - -import com.google.inject.Injector; -import com.google.inject.Key; -import io.dropwizard.Application; -import io.dropwizard.Configuration; -import io.dropwizard.testing.DropwizardTestSupport; -import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.TestExtensionsTracker; - -import javax.annotation.Nullable; - -/** - * Utility class combining test-framework agnostic utilities. - *

          - *
        • {@link DropwizardTestSupport} factory - *
        • {@link GuiceyTestSupport} factory (same as previous but without web part starting) - *
        • {@link ClientSupport} factory (web client) - *
        • Guice-related utilities like {@link Injector} or beans lookup - *
        • Utility methods for running before and after methods in one call (useful for error situation testing). - *
        - * - * @author Vyacheslav Rusakov - * @since 09.02.2022 - */ -public final class TestSupport { - - private TestSupport() { - } - - /** - * Creates {@link DropwizardTestSupport} instance for application configured from configuration file. - * {@link DropwizardTestSupport} starts complete dropwizard application including web part. Suitable - * for testing rest or servlet endpoints. For web-less application start see {@link #coreApp(Class, String)}. - *

        - * Note: this is just a most common use-case, for more complex cases instantiate object manually using - * different constructor. - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param configuration type - * @return dropwizard test support instance - */ - public static DropwizardTestSupport webApp( - final Class> appClass, final @Nullable String configPath) { - return new DropwizardTestSupport<>(appClass, configPath); - } - - /** - * Creates {@link GuiceyTestSupport} instance for application configured from configuration file. It is - * pre-configured {@link DropwizardTestSupport} instance (derivative class) starting only core application - * part (guice context) without web part. Suitable for testing core logic. - *

        - * Note: this is just a most common use-case, for more complex cases instantiate object manually using - * different constructor. - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param configuration type - * @return guicey test support instance - */ - public static GuiceyTestSupport coreApp( - final Class> appClass, final @Nullable String configPath) { - return new GuiceyTestSupport<>(appClass, configPath); - } - - /** - * Factory method for creating helper web client. Client is aware of dropwizard configuration and allows - * easy calling main/rest/admin contexts. Could also be used as a generic web client (for remote endpoints calls). - *

        - * Note that instance must be closed after usage, for example with try-with-resources: - * {@code try(ClientSupport client = TestSupport.webClient(support)) {...}}. - * - * @param support test support object (dropwizard or guicey) - * @return client support instance - */ - public static ClientSupport webClient(final DropwizardTestSupport support) { - return new ClientSupport(support); - } - - /** - * @param support test support object (dropwizard or guicey) - * @return application injector instance - */ - public static Injector getInjector(final DropwizardTestSupport support) { - return InjectorLookup.getInjector(support.getApplication()) - .orElseThrow(() -> new IllegalStateException("Injector not available")); - } - - /** - * Shortcut for accessing guice beans. - * - * @param support test support object (dropwizard or guicey) - * @param type target bean type - * @param bean type - * @return bean instance - */ - public static T getBean(final DropwizardTestSupport support, final Class type) { - return getBean(support, Key.get(type)); - } - - /** - * Shortcut for accessing guice beans. - * - * @param support test support object (dropwizard or guicey) - * @param key binding key - * @param bean type - * @return bean instance - */ - public static T getBean(final DropwizardTestSupport support, final Key key) { - return getInjector(support).getInstance(key); - } - - /** - * Shortcut method to apply field injections into target object instance. Useful to initialize test class - * fields (under not supported test frameworks). - * - * @param support test support object (dropwizard or guicey) - * @param target target instance to inject beans - */ - public static void injectBeans(final DropwizardTestSupport support, final Object target) { - getInjector(support).injectMembers(target); - } - - - /** - * Normally, {@link DropwizardTestSupport#before()} and {@link DropwizardTestSupport#after()} methods are called - * separately. This method is a shortcut mostly for errors testing when {@link DropwizardTestSupport#before()} - * assumed to fail to make sure {@link DropwizardTestSupport#after()} will be called in any case. - * - * @param callback callback (may be null) - * @param result type - * @param support test support instance - * @return callback result - * @throws Exception any appeared exception - */ - public static T run(final DropwizardTestSupport support, - final @Nullable RunCallback callback) throws Exception { - support.before(); - try { - return callback != null ? callback.run(getInjector(support)) : null; - } finally { - support.after(); - } - } - - /** - * Shortcut for web application startup test (replacing - * {@code TestSupport.execute(TestSupport.webApp(App.class, path)}). - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param configuration type - * @throws Exception any appeared exception - */ - public static void runWebApp(final Class> appClass, - final @Nullable String configPath) throws Exception { - runWebApp(appClass, configPath, null); - } - - /** - * Shortcut for web application startup test (replacing - * {@code TestSupport.execute(TestSupport.webApp(App.class, path), callback)}). - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param callback callback to execute while application started (may be null) - * @param configuration type - * @param result type - * @return callback result - * @throws Exception any appeared exception - */ - public static T runWebApp(final Class> appClass, - final @Nullable String configPath, - final @Nullable RunCallback callback) - throws Exception { - return run(webApp(appClass, configPath), callback); - } - - /** - * Shortcut for core application startup test (replacing - * {@code TestSupport.execute(TestSupport.coreApp(App.class, path))}). - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param configuration type - * @throws Exception any appeared exception - */ - public static void runCoreApp(final Class> appClass, - final @Nullable String configPath) throws Exception { - runCoreApp(appClass, configPath, null); - } - - /** - * Shortcut for core application startup test (replacing - * {@code TestSupport.execute(TestSupport.coreApp(App.class, path), callback)}). - * - * @param appClass application class - * @param configPath configuration file path (absolute or relative to working dir) (may be null) - * @param callback callback to execute while application started (may be null) - * @param configuration type - * @param result type - * @return callback result - * @throws Exception any appeared exception - */ - public static T runCoreApp(final Class> appClass, - final @Nullable String configPath, - final @Nullable RunCallback callback) - throws Exception { - return run(coreApp(appClass, configPath), callback); - } - - /** - * Enables debug output for registered junit 5 extensions. Simple alias for: - * {@code System.setProperty("guicey.extensions.debug", "true")}. - *

        - * Alternatively, debug could be enabled on extension directly with debug option. - */ - public static void debugExtensions() { - System.setProperty(TestExtensionsTracker.GUICEY_EXTENSIONS_DEBUG, "true"); - } - - /** - * Callback interface used for utility run application methods in {@link TestSupport}. - * - * @param result type - */ - @FunctionalInterface - public interface RunCallback { - - /** - * Execute custom logic while application started (using {@link DropwizardTestSupport} or - * {@link GuiceyTestSupport}). - * - * @param injector application injector - * @return value or null - * @throws Exception errors propagated - */ - T run(Injector injector) throws Exception; - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java b/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java deleted file mode 100644 index 41268553a..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/env/TestExtension.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.env; - -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionConfig; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.ExtensionBuilder; - -/** - * Configuration object for {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} objects. - * - * @author Vyacheslav Rusakov - * @since 15.05.2022 - */ -public class TestExtension extends ExtensionBuilder { - - public TestExtension(final ExtensionConfig cfg) { - super(cfg); - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java b/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java deleted file mode 100644 index 750bef150..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/GuiceyExtensionsSupport.java +++ /dev/null @@ -1,342 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.ext; - -import com.google.common.base.Preconditions; -import com.google.inject.Injector; -import io.dropwizard.testing.DropwizardTestSupport; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.platform.commons.support.AnnotationSupport; -import org.junit.platform.commons.util.ReflectionUtils; -import ru.vyarus.dropwizard.guice.hook.ConfigurationHooksSupport; -import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.injector.lookup.InjectorLookup; -import ru.vyarus.dropwizard.guice.test.ClientSupport; -import ru.vyarus.dropwizard.guice.test.EnableHook; -import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.conf.TestExtensionsTracker; -import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils; -import ru.vyarus.dropwizard.guice.test.util.HooksUtil; -import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Base class for junit 5 extensions implementations. All extensions use {@link DropwizardTestSupport} object - * for actual execution (only configuration differs). - *

        - * Extensions might be used on class level (annotation and manual registration in static field; when extension start - * dropwizard app before all tests and shut down it after all tests) and on method level (manual registration in non - * static field; application starts before each test). - *

        - * Nested tests also supported. - *

        - * Test instance is not managed by guice! Only {@link com.google.inject.Injector#injectMembers(Object)} applied - * for it to process test fields injection. Guice AOP can't be used on test methods. Technically, creating test - * instances with guice is possible, but in this case nested tests could not work at all, which is unacceptable. - *

        - * Extension detects static fields of {@link GuiceyConfigurationHook} type, annotated with {@link EnableHook} - * and initialize these hooks automatically. It was done like this to simplify customizations, when main extension - * could be declared as annotation and hook as field. Also, it was impossible to implement hooks support - * with junit extension. Hook field could be declared even in base test class. - *

        - * Also, detects {@link ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup} fields annotated with - * {@link ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup}. Behaviour is the same as with hook fields. - *

        - * For external integrations (other extensions), there is a special "hack" allowing to access - * {@link DropwizardTestSupport} object (and so get access to injector): {@link #lookupSupport(ExtensionContext)}. - * And shortcuts {@link #lookupInjector(ExtensionContext)} and {@link #lookupClient(ExtensionContext)}. - * - * @author Vyacheslav Rusakov - * @see TestParametersSupport for supported test parameters - * @since 29.04.2020 - */ -public abstract class GuiceyExtensionsSupport extends TestParametersSupport implements - BeforeAllCallback, - AfterAllCallback, - BeforeEachCallback, - AfterEachCallback { - - // dropwizard support storage key (store visible for all relative tests) - private static final String DW_SUPPORT = "DW_SUPPORT"; - // ClientFactory instance - private static final String DW_CLIENT = "DW_CLIENT"; - // indicator storage key of nested test (when extension activated in parent test) - private static final String INHERITED_DW_SUPPORT = "INHERITED_DW_SUPPORT"; - // indicator storage key for case when application started for each method in test - private static final String PER_METHOD_DW_SUPPORT = "PER_METHOD_DW_SUPPORT"; - - protected final TestExtensionsTracker tracker; - - public GuiceyExtensionsSupport(final TestExtensionsTracker tracker) { - this.tracker = tracker; - } - - @Override - public void beforeAll(final ExtensionContext context) throws Exception { - if (!lookupSupport(context).isPresent()) { - start(context, null); - } else { - // in case of nested test, beforeAll for root extension will be called second time (because junit keeps - // only one extension instance!) and this means we should not perform initialization, but we also must - // prevent afterAll call for this nested test too and so need to store marker value! - - final ExtensionContext.Store localStore = getLocalExtensionStore(context); - // just in case - Preconditions.checkState(localStore.get(INHERITED_DW_SUPPORT) == null, - "Storage assumptions were wrong or unexpected junit usage appear. " - + "Please report this case to guicey developer."); - localStore.put(INHERITED_DW_SUPPORT, true); - } - } - - @Override - public void beforeEach(final ExtensionContext context) throws Exception { - // run-per-method support (activated with @RegisterExtension on non-static field only) - if (!lookupSupport(context).isPresent()) { - start(context, context.getTestInstance().get()); - // mark per-method mode to properly shut down after test method - getLocalExtensionStore(context).put(PER_METHOD_DW_SUPPORT, true); - } - - // before each used to properly handle both default @TestInstance(TestInstance.Lifecycle.PER_METHOD) - // and @TestInstance(TestInstance.Lifecycle.PER_CLASS) (in later case BeforeAllCallback called after - // TestInstancePostProcessor, making it not usable for this task) - - final Object testInstance = context.getTestInstance() - .orElseThrow(() -> new IllegalStateException("Unable to get the current test instance")); - - final DropwizardTestSupport support = Preconditions.checkNotNull(getSupport(context), - "Guicey test support was not initialized: most likely, you are trying to manually " - + "register extension using non-static field - such usage is not supported."); - - InjectorLookup.getInjector(support.getApplication()).orElseThrow(() -> - new IllegalStateException("Can't find guicey injector to process test fields injections")) - .injectMembers(testInstance); - } - - @Override - public void afterEach(final ExtensionContext context) throws Exception { - if (getLocalExtensionStore(context).get(PER_METHOD_DW_SUPPORT) != null) { - stop(context); - } - } - - @Override - public void afterAll(final ExtensionContext context) throws Exception { - // do nothing in application per test method mode - if (lookupSupport(context).isPresent()) { - - // nested tests support - final Object nestedTestMarker = getLocalExtensionStore(context).remove(INHERITED_DW_SUPPORT); - if (nestedTestMarker != null) { - // do nothing: extension managed on upper context - return; - } - - stop(context); - } - } - - // --------------------------------------------------------- 3rd party extensions support - - /** - * Static "hack" for other extensions extending base guicey extensions abilities. - *

        - * The only thin moment here is extensions order! Junit preserve declaration order so in most cases it - * should not be a problem. - * - * @param extensionContext extension context - * @return dropwizard support object prepared by guicey extension, or null if no guicey extension used or - * its beforeAll hook was not called yet - */ - public static Optional> lookupSupport(final ExtensionContext extensionContext) { - return Optional.ofNullable((DropwizardTestSupport) getExtensionStore(extensionContext).get(DW_SUPPORT)); - } - - /** - * Shortcut for application injector resolution be used by other extensions. - *

        - * Custom extension must be activated after main guicey extension! - * - * @param extensionContext extension context - * @return application injector or null if not available - */ - public static Optional lookupInjector(final ExtensionContext extensionContext) { - return lookupSupport(extensionContext).flatMap(it -> InjectorLookup.getInjector(it.getApplication())); - } - - /** - * Shortcut for {@link ClientSupport} object lookup by other extensions. - *

        - * Custom extension must be activated after main guicey extension! - * - * @param extensionContext extension context - * @return client factory object or null if not available - */ - public static Optional lookupClient(final ExtensionContext extensionContext) { - return Optional.ofNullable((ClientSupport) getExtensionStore(extensionContext).get(DW_CLIENT)); - } - - // --------------------------------------------------------- end of 3rd party extensions support - - /** - * The only role of actual extension class is to configure {@link DropwizardTestSupport} object - * according to annotation. - * - * @param configPrefix configuration properties prefix - * @param context extension context - * @param setups setup extensions resolved from fields (or empty list) - * @return configured dropwizard test support object - */ - protected abstract DropwizardTestSupport prepareTestSupport(String configPrefix, - ExtensionContext context, - List setups); - - @Override - protected DropwizardTestSupport getSupport(final ExtensionContext extensionContext) { - return lookupSupport(extensionContext).orElse(null); - } - - @Override - protected ClientSupport getClient(final ExtensionContext extensionContext) { - return lookupClient(extensionContext).orElse(null); - } - - @Override - protected Optional getInjector(final ExtensionContext extensionContext) { - return lookupInjector(extensionContext); - } - - protected static ExtensionContext.Store getExtensionStore(final ExtensionContext context) { - // Store is extension specific, but nested tests will see it too (because key is extension class) - return context.getStore(ExtensionContext.Namespace.create(GuiceyExtensionsSupport.class)); - } - - private ExtensionContext.Store getLocalExtensionStore(final ExtensionContext context) { - // test scoped extension scope (required to differentiate nested classes or parameterized executions) - return context.getStore(ExtensionContext.Namespace - .create(GuiceyExtensionsSupport.class, context.getRequiredTestClass())); - } - - private void start(final ExtensionContext context, final Object testInstance) throws Exception { - final ExtensionContext.Store store = getExtensionStore(context); - // find fields annotated with @EnableHook and @EnableSetup - final FieldSupport fields = new FieldSupport(context.getRequiredTestClass(), testInstance, tracker); - fields.activateBaseHooks(); - - // config overrides work through system properties so it is important to have unique prefixes - final String configPrefix = ConfigOverrideUtils.createPrefix(context); - final DropwizardTestSupport support = prepareTestSupport(configPrefix, context, fields.getSetupObjects()); - // activate hooks declared in test static fields (so hooks declared in annotation goes before) - fields.activateClassHooks(); - store.put(DW_SUPPORT, support); - // for pure guicey tests client may seem redundant, but it can be used for calling other services - store.put(DW_CLIENT, new ClientSupport(support)); - - tracker.enableDebugFromSystemProperty(); - tracker.logUsedHooksAndSetupObjects(configPrefix); - support.before(); - tracker.logOverriddenConfigs(configPrefix); - } - - private void stop(final ExtensionContext context) throws Exception { - // just in case, normally hooks cleared automatically after appliance - ConfigurationHooksSupport.reset(); - - final DropwizardTestSupport support = getSupport(context); - if (support != null) { - support.after(); - } - final ClientSupport client = getClient(context); - if (client != null) { - client.close(); - } - } - - /** - * Utility class for activating hooks and setup objects collected from fields (annotated with - * {@link EnableHook} and {link {@link ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup}}). - *

        - * Hook fields must be activated in two steps: first hooks declared in base classes, then hooks declared directly - * in test class (after hooks declared in extension would be activated). - *

        - * Setup extensions from fields are always registered after all other. - */ - private static class FieldSupport { - // test instance in application per test method case (beforeEach) - private final Object instance; - private final TestExtensionsTracker tracker; - private final List parentHookFields; - private final List ownHookFields; - private final List extensionFields; - - FieldSupport(final Class testClass, final Object instance, final TestExtensionsTracker tracker) { - this.instance = instance; - this.tracker = tracker; - // find and validate all fields - final boolean includeInstanceFields = instance != null; - ownHookFields = findHookFields(testClass, includeInstanceFields); - parentHookFields = ownHookFields.isEmpty() ? Collections.emptyList() : ownHookFields.stream() - .filter(field -> !testClass.equals(field.getDeclaringClass())) - .collect(Collectors.toList()); - ownHookFields.removeAll(parentHookFields); - - extensionFields = findSetupFields(testClass, includeInstanceFields); - } - - public List getSetupObjects() { - tracker.extensionsFromFields(extensionFields, instance); - return extensionFields.isEmpty() ? Collections.emptyList() : getFieldValues(extensionFields); - } - - public void activateBaseHooks() { - // activate hooks declared in base classes - activateFieldHooks(parentHookFields); - tracker.hooksFromFields(parentHookFields, true, instance); - } - - public void activateClassHooks() { - // activate all remaining hooks (in test class) - activateFieldHooks(ownHookFields); - tracker.hooksFromFields(ownHookFields, false, instance); - } - - private void activateFieldHooks(final List fields) { - HooksUtil.register(getFieldValues(fields)); - } - - @SuppressWarnings("unchecked") - private List getFieldValues(final List fields) { - return fields.isEmpty() ? Collections.emptyList() : (List) - ReflectionUtils.readFieldValues(fields, instance); - } - - private List findHookFields(final Class testClass, final boolean includeInstanceFields) { - List fields = AnnotationSupport.findAnnotatedFields(testClass, EnableHook.class); - if (includeInstanceFields) { - fields = new ArrayList<>(fields); // original list is unmodifiable - // sort static fields first - fields.sort(Comparator.comparing(field -> Modifier.isStatic(field.getModifiers()) ? 0 : 1)); - } - HooksUtil.validateFieldHooks(fields, includeInstanceFields); - return fields.isEmpty() ? Collections.emptyList() : new ArrayList<>(fields); - } - - private List findSetupFields(final Class testClass, final boolean includeInstanceFields) { - final List fields = AnnotationSupport.findAnnotatedFields(testClass, EnableSetup.class); - TestSetupUtils.validateFields(fields, includeInstanceFields); - return fields.isEmpty() ? Collections.emptyList() : new ArrayList<>(fields); - } - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java b/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java deleted file mode 100644 index a256808ad..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/ExtensionConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf; - -import io.dropwizard.testing.ConfigOverride; -import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.util.HooksUtil; -import ru.vyarus.dropwizard.guice.test.util.TestSetupUtils; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Base configuration for junit 5 extensions (contains common configurations). Required to unify common configuration - * methods in {@link ExtensionBuilder}. - * - * @author Vyacheslav Rusakov - * @since 12.05.2022 - */ -@SuppressWarnings({"checkstyle:VisibilityModifier", "PMD.DefaultPackage"}) -public abstract class ExtensionConfig { - - public String[] configOverrides = new String[0]; - // required for lazy evaluation values - public final List configOverrideObjects = new ArrayList<>(); - public final List hooks = new ArrayList<>(); - - public final List extensions = new ArrayList<>(); - // tracks source of registered setup objects - - public final TestExtensionsTracker tracker; - - public ExtensionConfig(final TestExtensionsTracker tracker) { - this.tracker = tracker; - } - - - @SafeVarargs - public final void extensionsFromAnnotation(final Class ann, - final Class... exts) { - extensions.addAll(TestSetupUtils.create(exts)); - tracker.extensionsFromAnnotation(ann, exts); - } - - @SafeVarargs - public final void hooksFromAnnotation(final Class ann, - final Class... exts) { - hooks.addAll(HooksUtil.create(exts)); - tracker.hooksFromAnnotation(ann, exts); - } - - public final void hookInstances(final GuiceyConfigurationHook... exts) { - Collections.addAll(hooks, exts); - tracker.hookInstances(exts); - } - - @SafeVarargs - public final void hookClasses(final Class... exts) { - hooks.addAll(HooksUtil.create(exts)); - tracker.hookClasses(exts); - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/TestExtensionsTracker.java b/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/TestExtensionsTracker.java deleted file mode 100644 index 0c8bced3b..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/jupiter/ext/conf/TestExtensionsTracker.java +++ /dev/null @@ -1,171 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.ext.conf; - -import org.junit.jupiter.api.extension.RegisterExtension; -import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; -import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.test.EnableHook; -import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.util.RegistrationTrackUtils; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Tracks registration of hooks and support objects during test initialization in order to log used - * additions (to simplify applied objects tracking). Also, tracks applied configuration overrides, but only after - * application start (the only way to show actually applied values). - * - * @author Vyacheslav Rusakov - * @since 27.05.2022 - */ -@SuppressWarnings({"checkstyle:MultipleStringLiterals", "PMD:AvoidDuplicateLiterals"}) -public class TestExtensionsTracker { - - /** - * System property enables debug output for all used guicey extensions. - */ - public static final String GUICEY_EXTENSIONS_DEBUG = "guicey.extensions.debug"; - /** - * Enabled value for {@link #GUICEY_EXTENSIONS_DEBUG} system property. - */ - public static final String DEBUG_ENABLED = "true"; - - @SuppressWarnings("checkstyle:VisibilityModifier") - public boolean debug; - - protected final List extensionsSource = new ArrayList<>(); - protected final List hooksSource = new ArrayList<>(); - - private Class contextHook; - - public void setContextHook(final Class hook) { - contextHook = hook; - } - - public final void extensionsFromFields(final List fields, final Object instance) { - RegistrationTrackUtils.fromField(extensionsSource, "@" + EnableSetup.class.getSimpleName(), fields, instance); - } - - @SafeVarargs - public final void extensionsFromAnnotation(final Class ann, - final Class... exts) { - // sync actual extension registration order with tracking info - final List tmp = new ArrayList<>(extensionsSource); - extensionsSource.clear(); - RegistrationTrackUtils.fromClass(extensionsSource, "@" + ann.getSimpleName(), exts); - extensionsSource.addAll(tmp); - } - - public final void hooksFromFields(final List fields, final boolean baseHooks, final Object instance) { - if (!fields.isEmpty()) { - // hooks from fields in base classes activated before configured hooks - final List tmp = baseHooks ? new ArrayList<>(hooksSource) : Collections.emptyList(); - if (baseHooks) { - hooksSource.clear(); - } - RegistrationTrackUtils.fromField(hooksSource, "@" + EnableHook.class.getSimpleName(), fields, instance); - hooksSource.addAll(tmp); - } - } - - @SafeVarargs - public final void hooksFromAnnotation(final Class ann, - final Class... exts) { - RegistrationTrackUtils.fromClass(hooksSource, "@" + ann.getSimpleName(), exts); - } - - public final void extensionInstances(final TestEnvironmentSetup... exts) { - RegistrationTrackUtils.fromInstance(extensionsSource, String.format("@%s instance", - RegisterExtension.class.getSimpleName()), exts); - } - - @SafeVarargs - public final void extensionClasses(final Class... exts) { - RegistrationTrackUtils.fromClass(extensionsSource, String.format("@%s class", - RegisterExtension.class.getSimpleName()), exts); - } - - public final void hookInstances(final GuiceyConfigurationHook... exts) { - RegistrationTrackUtils.fromInstance(hooksSource, String.format("%s instance", getHookContext()), exts); - } - - @SafeVarargs - public final void hookClasses(final Class... exts) { - RegistrationTrackUtils.fromClass(hooksSource, String.format("%s class", getHookContext()), exts); - } - - /** - * In some cases it might be simpler to use system property to enable debug: {@code -Dguicey.extensions.debug=true}. - */ - public void enableDebugFromSystemProperty() { - if (!debug && DEBUG_ENABLED.equalsIgnoreCase(System.getProperty(GUICEY_EXTENSIONS_DEBUG))) { - debug = true; - } - } - - /** - * Logs registered setup objects and hooks. Do nothing if no setup objects or hooks registered. - * - * @param configPrefix configuration prefix - */ - @SuppressWarnings("PMD.SystemPrintln") - public void logUsedHooksAndSetupObjects(final String configPrefix) { - if (debug && (!extensionsSource.isEmpty() || !hooksSource.isEmpty())) { - // using config prefix to differentiate outputs for parallel execution - final StringBuilder res = new StringBuilder(500).append("\nGuicey test extensions (") - .append(configPrefix).append(".):\n\n"); - if (!extensionsSource.isEmpty()) { - res.append("\tSetup objects = \n"); - logTracks(res, extensionsSource); - } - - if (!hooksSource.isEmpty()) { - res.append("\tTest hooks = \n"); - logTracks(res, hooksSource); - } - - System.out.println(res); - } - } - - /** - * Logs overridden configurations. Show values already applied to system properties. - * - * @param configPrefix configuration prefix - */ - @SuppressWarnings("PMD.SystemPrintln") - public void logOverriddenConfigs(final String configPrefix) { - if (debug) { - final StringBuilder res = new StringBuilder(); - for (Map.Entry entry : System.getProperties().entrySet()) { - final String key = (String) entry.getKey(); - if (key.startsWith(configPrefix)) { - res.append(String.format("\t %20s = %s%n", - key.substring(configPrefix.length() + 1), entry.getValue())); - } - } - if (res.length() > 0) { - System.out.println("\nApplied configuration overrides (" + configPrefix + ".): \n\n" + res); - } - } - } - - private String getHookContext() { - // hook might be registered from manual extension in filed or within setup object and in this case - // tracking setup object class - return contextHook != null - ? RenderUtils.getClassName(contextHook) : "@" + RegisterExtension.class.getSimpleName(); - } - - private void logTracks(final StringBuilder res, final List tracks) { - for (String st : tracks) { - res.append("\t\t").append(st).append('\n'); - } - res.append('\n'); - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java b/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java deleted file mode 100644 index f9affff86..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/HooksUtil.java +++ /dev/null @@ -1,80 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.util; - -import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.test.EnableHook; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; - -/** - * Guicey {@link ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook} test utilities. - * - * @author Vyacheslav Rusakov - * @since 02.05.2020 - */ -public final class HooksUtil { - - private HooksUtil() { - } - - /** - * Validate fields annotated with {@link EnableHook} for correctness. - * - * @param fields fields to validate - * @param includeInstanceFields true to allow instance fields, false to break if instance field detected - */ - public static void validateFieldHooks(final List fields, final boolean includeInstanceFields) { - for (Field field : fields) { - if (!GuiceyConfigurationHook.class.isAssignableFrom(field.getType())) { - throw new IllegalStateException(String.format( - "Field %s annotated with @%s, but its type is not %s", - toString(field), EnableHook.class.getSimpleName(), GuiceyConfigurationHook.class.getSimpleName() - )); - } - if (!includeInstanceFields && !Modifier.isStatic(field.getModifiers())) { - throw new IllegalStateException(String.format("Field %s annotated with @%s must be static", - toString(field), EnableHook.class.getSimpleName())); - } - } - } - - /** - * Instantiates provided hooks. - * - * @param hooks hooks to instantiate - * @return hooks instances - */ - @SafeVarargs - public static List create(final Class... hooks) { - final List res = new ArrayList<>(); - for (Class hook : hooks) { - try { - res.add(hook.newInstance()); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate guicey hook: " + hook.getSimpleName(), e); - } - } - return res; - } - - /** - * Register configuration hooks. - * - * @param hooks hooks to register - */ - public static void register(final List hooks) { - if (hooks != null) { - for (GuiceyConfigurationHook hook : hooks) { - if (hook != null) { - hook.register(); - } - } - } - } - - private static String toString(final Field field) { - return field.getDeclaringClass().getName() + "." + field.getName(); - } -} diff --git a/src/main/java/ru/vyarus/dropwizard/guice/test/util/RegistrationTrackUtils.java b/src/main/java/ru/vyarus/dropwizard/guice/test/util/RegistrationTrackUtils.java deleted file mode 100644 index 808d25f0c..000000000 --- a/src/main/java/ru/vyarus/dropwizard/guice/test/util/RegistrationTrackUtils.java +++ /dev/null @@ -1,73 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.util; - -import org.junit.platform.commons.util.ReflectionUtils; -import ru.vyarus.dropwizard.guice.debug.util.RenderUtils; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - -/** - * Utilities for rendering registration tracking info for hooks and setup test objects. - * - * @author Vyacheslav Rusakov - * @since 20.05.2022 - */ -@SuppressWarnings("PMD.UseVarargs") -public final class RegistrationTrackUtils { - - private RegistrationTrackUtils() { - } - - /** - * Stores tracking info for registered classes. - * - * @param info info holder - * @param prefix source identity - * @param classes items to append - */ - public static void fromClass(final List info, final String prefix, final Class[] classes) { - track(info, Arrays.asList(classes), it -> it, it -> prefix); - } - - /** - * Stores tracking info for registered instances. - * - * @param info info holder - * @param prefix source identity - * @param instances instances to append - */ - public static void fromInstance(final List info, final String prefix, final Object[] instances) { - track(info, Arrays.asList(instances), Object::getClass, obj -> prefix); - } - - /** - * Stores tracking info for recognized test class fields. - * - * @param info info holder - * @param prefix source identity - * @param fields fields to append - * @param instance test instance or null for static fields - */ - public static void fromField(final List info, - final String prefix, - final List fields, - final Object instance) { - track(info, fields, - field -> ReflectionUtils.tryToReadFieldValue(field, instance) - .orElseTry(() -> new Exception()).toOptional().map(Object::getClass).get(), - field -> prefix + " field " + field.getDeclaringClass().getSimpleName() + "." + field.getName() - ); - } - - private static void track(final List info, - final List objects, - final Function converter, - final Function marker) { - for (T obj : objects) { - final Class cls = converter.apply(obj); - info.add(String.format("%-80s \t%s", RenderUtils.renderClassLine(cls), marker.apply(obj))); - } - } -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy b/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy deleted file mode 100644 index 7659e91d7..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/bundles/EnvironmentListenersShortcutsTest.groovy +++ /dev/null @@ -1,99 +0,0 @@ -package ru.vyarus.dropwizard.guice.bundles - -import io.dropwizard.Application -import io.dropwizard.Configuration -import io.dropwizard.lifecycle.Managed -import io.dropwizard.lifecycle.ServerLifecycleListener -import io.dropwizard.setup.Bootstrap -import io.dropwizard.setup.Environment -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.util.component.LifeCycle -import ru.vyarus.dropwizard.guice.GuiceBundle -import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle -import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyEnvironment -import ru.vyarus.dropwizard.guice.test.jupiter.TestDropwizardApp -import spock.lang.Specification - -/** - * @author Vyacheslav Rusakov - * @since 18.09.2019 - */ -@TestDropwizardApp(App) -class EnvironmentListenersShortcutsTest extends Specification { - - def "Check listeners registration"() { - - expect: "listeners called" - Mng.called - LListener.called - SListener.called - Bundle.onGuiceyStartup - Bundle.onStartup - } - - static class App extends Application { - @Override - void initialize(Bootstrap bootstrap) { - bootstrap.addBundle(GuiceBundle.builder() - .bundles(new Bundle()) - .build()) - } - - @Override - void run(Configuration configuration, Environment environment) throws Exception { - } - } - - static class Bundle implements GuiceyBundle { - static boolean onStartup - static boolean onGuiceyStartup - - @Override - void run(GuiceyEnvironment environment) { - environment.manage(new Mng()) - environment.listenJetty(new LListener()) - environment.listenServer(new SListener()) - environment.onGuiceyStartup({ cfg, env, inj -> - onGuiceyStartup = true - assert cfg != null - assert env != null - assert inj != null - }) - environment.onApplicationStartup({ inj -> - onStartup = true - assert inj != null - }) - } - } - - static class Mng implements Managed { - static boolean called - - @Override - void start() throws Exception { - called = true - } - - @Override - void stop() throws Exception { - } - } - - static class LListener implements LifeCycle.Listener { - static boolean called - - @Override - void lifeCycleStarted(LifeCycle event) { - called = true - } - } - - static class SListener implements ServerLifecycleListener { - static boolean called - - @Override - void serverStarted(Server server) { - called = true - } - } -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisablesPredicatesTest.groovy b/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisablesPredicatesTest.groovy deleted file mode 100644 index 7da25621b..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/DisablesPredicatesTest.groovy +++ /dev/null @@ -1,76 +0,0 @@ -package ru.vyarus.dropwizard.guice.config - -import io.dropwizard.Application -import ru.vyarus.dropwizard.guice.AbstractTest -import ru.vyarus.dropwizard.guice.module.context.ConfigItem -import ru.vyarus.dropwizard.guice.module.context.ConfigScope -import ru.vyarus.dropwizard.guice.module.context.Disables -import ru.vyarus.dropwizard.guice.module.context.info.ItemId -import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo -import ru.vyarus.dropwizard.guice.module.context.info.impl.ItemInfoImpl - -import static ru.vyarus.dropwizard.guice.module.context.ConfigItem.* - -/** - * @author Vyacheslav Rusakov - * @since 09.04.2018 - */ -class DisablesPredicatesTest extends AbstractTest { - - def "Check predicates"() { - - expect: - Disables.registeredBy(ConfigScope.Application).test(item(Extension, Sample)) - Disables.registeredBy(ConfigScope.Application.getKey()).test(item(Extension, Sample)) - !Disables.registeredBy(Serializable).test(item(Extension, Sample)) - !Disables.registeredBy(ItemId.from(Serializable)).test(item(Extension, Sample)) - - Disables.itemType(Extension, Installer).test(item(Installer, Sample)) - !Disables.itemType(Extension, Installer).test(item(Bundle, Sample)) - - Disables.extension().test(item(Extension, Sample)) - !Disables.extension().test(item(Installer, Sample)) - - Disables.installer().test(item(Installer, Sample)) - !Disables.installer().test(item(Extension, Sample)) - - Disables.module().test(item(ConfigItem.Module, Sample)) - !Disables.module().test(item(Extension, Sample)) - - Disables.bundle().test(item(Bundle, Sample)) - !Disables.bundle().test(item(Extension, Sample)) - - Disables.dropwizardBundle().test(item(DropwizardBundle, Sample)) - !Disables.dropwizardBundle().test(item(Bundle, Sample)) - - Disables.type(Sample).test(item(Extension, Sample)) - !Disables.type(Sample).test(item(Extension, Sample2)) - - Disables.inPackage('ru.vyarus').test(item(Extension, Sample)) - !Disables.inPackage('com.foo').test(item(Extension, Sample2)) - } - - def "Check composition"() { - - def predicate = Disables.registeredBy(Application) - .and(Disables.installer()) - .and(Disables.type(Sample2).negate()) - - expect: - predicate.test(item(Installer, Sample)) - !predicate.test(item(Extension, Sample)) - !predicate.test(item(Installer, Sample2)) - !predicate.test(item(Installer, Sample, Sample)) - } - - ItemInfo item(ConfigItem type, Class cls, Class from = Application) { - ItemInfoImpl info = new ItemInfoImpl(type, ItemId.from(cls)) - info.countRegistrationAttempt(ItemId.from(from)) - return info; - } - - - static class Sample {} - - static class Sample2 {} -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy b/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy deleted file mode 100644 index c92d22e3c..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/config/FiltersTest.groovy +++ /dev/null @@ -1,135 +0,0 @@ -package ru.vyarus.dropwizard.guice.config - -import io.dropwizard.Application -import io.dropwizard.ConfiguredBundle -import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup -import ru.vyarus.dropwizard.guice.module.context.ConfigItem -import ru.vyarus.dropwizard.guice.module.context.ConfigScope -import ru.vyarus.dropwizard.guice.module.context.Filters -import ru.vyarus.dropwizard.guice.debug.ConfigurationDiagnostic -import ru.vyarus.dropwizard.guice.module.context.info.ItemId -import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo -import ru.vyarus.dropwizard.guice.module.installer.feature.health.HealthCheckInstaller -import ru.vyarus.dropwizard.guice.module.installer.feature.jersey.JerseyFeatureInstaller -import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner -import ru.vyarus.dropwizard.guice.module.jersey.debug.HK2DebugBundle -import ru.vyarus.dropwizard.guice.module.jersey.debug.service.HK2DebugFeature -import spock.lang.Specification - -/** - * @author Vyacheslav Rusakov - * @since 26.07.2016 - */ -class FiltersTest extends Specification { - - def "Check enabled filter"() { - - expect: "disabled item filtered" - !Filters.enabled().test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - disabledBy.add(Application) - }) - - and: "not supported items enabled" - Filters.enabled().test(item(ConfigItem.Module, HK2DebugBundle.HK2DebugModule) {}) - - } - - def "Check disabledBy filter"() { - - expect: "matched check" - Filters.disabledBy(Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - disabledBy.add(ItemId.from(Application)) - }) - - and: "not matched check" - !Filters.disabledBy(Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - disabledBy.add(ItemId.from(ConfiguredBundle)) - }) - - } - - def "Check scan filter"() { - - expect: "from scan" - Filters.fromScan().test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - registeredBy.add(ItemId.from(ClasspathScanner)) - }) - - and: "not from scan" - !Filters.fromScan().test(item(ConfigItem.Installer, JerseyFeatureInstaller) {}) - - and: "item not support scan" - !Filters.fromScan().test(item(ConfigItem.Module, HK2DebugBundle.HK2DebugModule) {}) - - } - - def "Check registrationScope filter"() { - - expect: "matched check" - Filters.registrationScope(ConfigScope.Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - countRegistrationAttempt(ItemId.from(Application)) - }) - - and: "not matched check" - !Filters.registrationScope(Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - countRegistrationAttempt(ItemId.from(ConfiguredBundle)) - }) - - } - - def "Check registeredBy filter"() { - - expect: "matched check" - Filters.registeredBy(ConfigScope.Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - registeredBy.add(ItemId.from(Application)) - }) - - and: "not matched check" - !Filters.registeredBy(Application).test(item(ConfigItem.Installer, JerseyFeatureInstaller) { - registeredBy.add(ItemId.from(ConfiguredBundle)) - }) - - } - - def "Check type filter"() { - - expect: "matched check" - Filters.type(ConfigItem.Installer).test(item(ConfigItem.Installer, JerseyFeatureInstaller) {}) - Filters.type(JerseyFeatureInstaller).test(item(ConfigItem.Installer, JerseyFeatureInstaller) {}) - - and: "not matched check" - !Filters.type(ConfigItem.Bundle).test(item(ConfigItem.Installer, JerseyFeatureInstaller) {}) - !Filters.type(HealthCheckInstaller).test(item(ConfigItem.Installer, JerseyFeatureInstaller) {}) - - } - - def "Check lookupBundles filter"() { - - expect: "matched check" - Filters.lookupBundles().test(item(ConfigItem.Bundle, ConfigurationDiagnostic) { - registeredBy.add(ItemId.from(GuiceyBundleLookup)) - }) - - and: "not matched check" - !Filters.lookupBundles().test(item(ConfigItem.Bundle, ConfigurationDiagnostic) {}) - - } - - def "Check installedBy filter"() { - - expect: "matched check" - Filters.installedBy(JerseyFeatureInstaller).test(item(ConfigItem.Extension, HK2DebugFeature) { - installedBy = JerseyFeatureInstaller - }) - - and: "not matched check" - !Filters.installedBy(JerseyFeatureInstaller).test(item(ConfigItem.Extension, HK2DebugFeature) {}) - - } - - private static ItemInfo item(ConfigItem itemType, Class type, Closure init) { - ItemInfo info = itemType.newContainer(type) - info.with init - info - } -} \ No newline at end of file diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java b/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java deleted file mode 100644 index 61058661b..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/NestedPropagationTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter; - -import io.dropwizard.setup.Environment; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import ru.vyarus.dropwizard.guice.support.AutoScanApplication; - -import javax.inject.Inject; - -/** - * @author Vyacheslav Rusakov - * @since 01.05.2020 - */ -@TestGuiceyApp(AutoScanApplication.class) -public class NestedPropagationTest { - - @Inject - Environment environment; - - @Test - void checkInjection() { - Assertions.assertNotNull(environment); - } - - @Nested - class Inner { - - @Inject - Environment env; // intentionally different name - - @Test - void checkInjection() { - Assertions.assertNotNull(env); - } - } -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java b/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java deleted file mode 100644 index 08559b717..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/ConfigOverrideLogTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.debug; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.platform.testkit.engine.EngineTestKit; -import ru.vyarus.dropwizard.guice.support.AutoScanApplication; -import ru.vyarus.dropwizard.guice.test.TestSupport; -import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.stream.SystemOut; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; - -/** - * @author Vyacheslav Rusakov - * @since 25.06.2022 - */ -@ExtendWith(SystemStubsExtension.class) -public class ConfigOverrideLogTest { - - @SystemStub - SystemOut out; - - @Test - void checkSetupOutputForAnnotation() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test1.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output).contains("Applied configuration overrides (Test1.): \n" + - "\n" + - "\t foo = 1"); - } - - @Test - void checkSetupOutputForManualRegistration() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test2.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output).contains("Applied configuration overrides (Test2.): \n" + - "\n" + - "\t foo = 2"); - } - - @Disabled // prevent direct execution - @TestGuiceyApp(value = AutoScanApplication.class, configOverride = "foo: 1", debug = true) - public static class Test1 { - - @Test - void test() { - } - } - - @Disabled // prevent direct execution - public static class Test2 { - - @RegisterExtension - static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) - .configOverrides("foo: 2") - .debug() - .create(); - - @Test - void test() { - } - - } -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java b/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java deleted file mode 100644 index 11493c432..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/HookObjectsLogTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.debug; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.platform.testkit.engine.EngineTestKit; -import ru.vyarus.dropwizard.guice.GuiceBundle; -import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook; -import ru.vyarus.dropwizard.guice.support.AutoScanApplication; -import ru.vyarus.dropwizard.guice.test.EnableHook; -import ru.vyarus.dropwizard.guice.test.TestSupport; -import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; -import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.stream.SystemOut; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; - -/** - * @author Vyacheslav Rusakov - * @since 29.05.2022 - */ -@ExtendWith(SystemStubsExtension.class) -public class HookObjectsLogTest { - - @SystemStub - SystemOut out; - - @Test - void checkSetupOutputForAnnotation() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test1.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") - .replaceAll("\\) {8,}\t", ") \t")) - .contains("Guicey test extensions (Test1.):\n" + - "\n" + - "\tSetup objects = \n" + - "\t\tHookObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test1.setup\n" + - "\n" + - "\tTest hooks = \n" + - "\t\tHookObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Base.base1\n" + - "\t\tHookObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Base.base2\n" + - "\t\tExt1 (r.v.d.g.t.j.d.HookObjectsLogTest) \t@TestGuiceyApp\n" + - "\t\tExt2 (r.v.d.g.t.j.d.HookObjectsLogTest) \t@TestGuiceyApp\n" + - "\t\tExt3 (r.v.d.g.t.j.d.HookObjectsLogTest) \tHookObjectsLogTest$Test1$$Lambda$111/1111111 class\n" + - "\t\tExt4 (r.v.d.g.t.j.d.HookObjectsLogTest) \tHookObjectsLogTest$Test1$$Lambda$111/1111111 class\n" + - "\t\tHookObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \tHookObjectsLogTest$Test1$$Lambda$111/1111111 instance\n" + - "\t\tHookObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \tHookObjectsLogTest$Test1$$Lambda$111/1111111 instance\n" + - "\t\tHookObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Test1.ext1\n" + - "\t\tHookObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Test1.ext2\n"); - } - - @Test - void checkSetupOutputForManualRegistration() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test2.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") - .replaceAll("\\) {8,}\t", ") \t")) - .contains("Guicey test extensions (Test2.):\n" + - "\n" + - "\tSetup objects = \n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test2.setup\n" + - "\n" + - "\tTest hooks = \n" + - "\t\tHookObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Base.base1\n" + - "\t\tHookObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Base.base2\n" + - "\t\tExt1 (r.v.d.g.t.j.d.HookObjectsLogTest) \t@RegisterExtension class\n" + - "\t\tExt2 (r.v.d.g.t.j.d.HookObjectsLogTest) \t@RegisterExtension class\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@RegisterExtension instance\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@RegisterExtension instance\n" + - "\t\tExt3 (r.v.d.g.t.j.d.HookObjectsLogTest) \tHookObjectsLogTest$Test2$$Lambda$111/1111111 class\n" + - "\t\tExt4 (r.v.d.g.t.j.d.HookObjectsLogTest) \tHookObjectsLogTest$Test2$$Lambda$111/1111111 class\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \tHookObjectsLogTest$Test2$$Lambda$111/1111111 instance\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \tHookObjectsLogTest$Test2$$Lambda$111/1111111 instance\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Test2.ext1\n" + - "\t\tHookObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableHook field Test2.ext2\n"); - } - - public static class Base { - - @EnableHook - static GuiceyConfigurationHook base1 = it -> {}; - @EnableHook - static GuiceyConfigurationHook base2 = it -> {}; - } - - public static class Ext1 implements GuiceyConfigurationHook { - - @Override - public void configure(GuiceBundle.Builder builder) { - } - } - - public static class Ext2 extends Ext1 {} - - public static class Ext3 extends Ext1 {} - - public static class Ext4 extends Ext1 {} - - - @Disabled // prevent direct execution - @TestGuiceyApp(value = AutoScanApplication.class, hooks = {Ext1.class, Ext2.class}) - public static class Test1 extends Base { - - @EnableSetup - static TestEnvironmentSetup setup = it -> it - .hooks(Ext3.class, Ext4.class) - .hooks(t -> {}, t -> {}); - - @EnableHook - static GuiceyConfigurationHook ext1 = it -> {}; - @EnableHook - static GuiceyConfigurationHook ext2 = it -> {}; - - @Test - void test() { - } - } - - @Disabled // prevent direct execution - public static class Test2 extends Base { - - @RegisterExtension - static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) - .hooks(Ext1.class, Ext2.class) - .hooks(it -> {}, it -> {}) - .create(); - - @EnableSetup - static TestEnvironmentSetup setup = it -> it - .hooks(Ext3.class, Ext4.class) - .hooks(t -> {}, t -> {}); - - @EnableHook - static GuiceyConfigurationHook ext1 = it -> {}; - @EnableHook - static GuiceyConfigurationHook ext2 = it -> {}; - - @Test - void test() { - } - } -} diff --git a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java b/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java deleted file mode 100644 index 226ae2b02..000000000 --- a/src/test/groovy/ru/vyarus/dropwizard/guice/test/jupiter/debug/SetupObjectsLogTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package ru.vyarus.dropwizard.guice.test.jupiter.debug; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.platform.testkit.engine.EngineTestKit; -import ru.vyarus.dropwizard.guice.support.AutoScanApplication; -import ru.vyarus.dropwizard.guice.test.TestSupport; -import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp; -import ru.vyarus.dropwizard.guice.test.jupiter.env.EnableSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup; -import ru.vyarus.dropwizard.guice.test.jupiter.env.TestExtension; -import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestGuiceyAppExtension; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; -import uk.org.webcompere.systemstubs.stream.SystemOut; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; - -/** - * @author Vyacheslav Rusakov - * @since 29.05.2022 - */ -@ExtendWith(SystemStubsExtension.class) -public class SetupObjectsLogTest { - - @SystemStub - SystemOut out; - - @Test - void checkSetupOutputForAnnotation() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test1.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") - .replaceAll("\\) {8,}\t", ") \t")) - .contains("Guicey test extensions (Test1.):\n" + - "\n" + - "\tSetup objects = \n" + - "\t\tExt1 (r.v.d.g.t.j.d.SetupObjectsLogTest) \t@TestGuiceyApp\n" + - "\t\tExt2 (r.v.d.g.t.j.d.SetupObjectsLogTest) \t@TestGuiceyApp\n" + - "\t\tSetupObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Base.base1\n" + - "\t\tSetupObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Base.base2\n" + - "\t\tSetupObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test1.ext1\n" + - "\t\tSetupObjectsLogTest$Test1$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test1.ext2\n"); - } - - @Test - void checkSetupOutputForManualRegistration() { - TestSupport.debugExtensions(); - EngineTestKit - .engine("junit-jupiter") - .configurationParameter("junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition") - .selectors(selectClass(Test2.class)) - .execute() - .testEvents() - .debug() - .assertStatistics(stats -> stats.succeeded(1)); - - String output = out.getText().replace("\r", ""); - System.err.println(output); - - assertThat(output.replaceAll("\\$\\$Lambda\\$\\d+/\\d+(x[a-z\\d]+)?", "\\$\\$Lambda\\$111/1111111") - .replaceAll("\\) {8,}\t", ") \t")) - .contains("Guicey test extensions (Test2.):\n" + - "\n" + - "\tSetup objects = \n" + - "\t\tExt1 (r.v.d.g.t.j.d.SetupObjectsLogTest) \t@RegisterExtension class\n" + - "\t\tExt2 (r.v.d.g.t.j.d.SetupObjectsLogTest) \t@RegisterExtension class\n" + - "\t\tSetupObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@RegisterExtension instance\n" + - "\t\tSetupObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@RegisterExtension instance\n" + - "\t\tSetupObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Base.base1\n" + - "\t\tSetupObjectsLogTest$Base$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Base.base2\n" + - "\t\tSetupObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test2.ext1\n" + - "\t\tSetupObjectsLogTest$Test2$$Lambda$111/1111111 (r.v.d.g.t.j.debug) \t@EnableSetup field Test2.ext2\n"); - } - - public static class Base { - - @EnableSetup - static TestEnvironmentSetup base1 = it -> null; - @EnableSetup - static TestEnvironmentSetup base2 = it -> null; - } - - public static class Ext1 implements TestEnvironmentSetup { - @Override - public Object setup(TestExtension extension) { - return null; - } - } - - public static class Ext2 extends Ext1 {} - - @Disabled // prevent direct execution - @TestGuiceyApp(value = AutoScanApplication.class, setup = {Ext1.class, Ext2.class}) - public static class Test1 extends Base { - - @EnableSetup - static TestEnvironmentSetup ext1 = it -> null; - @EnableSetup - static TestEnvironmentSetup ext2 = it -> null; - - @Test - void test() { - } - } - - @Disabled // prevent direct execution - public static class Test2 extends Base { - - @RegisterExtension - static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(AutoScanApplication.class) - .setup(Ext1.class, Ext2.class) - .setup(it -> null, it -> null) - .create(); - - @EnableSetup - static TestEnvironmentSetup ext1 = it -> null; - @EnableSetup - static TestEnvironmentSetup ext2 = it -> null; - - @Test - void test() { - } - } -}