feat: introduce WebSocket support with new endpoints and transaction propagation enhancements
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
name: Deploy Publish JavaDoc
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Temurin 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 21
|
||||||
|
cache: maven
|
||||||
|
|
||||||
|
- name: Build release artifacts
|
||||||
|
run: mvn -B -DskipTests clean verify
|
||||||
|
|
||||||
|
- name: Generate aggregated JavaDoc
|
||||||
|
run: mvn -B -DskipTests -pl flash,flash-extensions -am javadoc:aggregate
|
||||||
|
|
||||||
|
- name: Publish JavaDoc
|
||||||
|
uses: MathieuSoysal/Javadoc-publisher.yml@v3.0.2
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
javadoc-branch: javadoc
|
||||||
|
java-version: 21
|
||||||
|
target-folder: javadoc
|
||||||
|
project: maven
|
||||||
|
custom-command: mvn -B -DskipTests -pl flash,flash-extensions -am javadoc:aggregate -DskipTests
|
||||||
Generated
+172
-340
@@ -4,292 +4,95 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="add core view extension with JTE and Thymeleaf support">
|
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="feat: introduce Spec and Query interfaces with transaction propagation enhancements">
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/pom.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/.github/workflows/publish-javadoc.yml" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/benchmarks/websocket/benchmark.yml" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Page.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/docs/README.md" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Query.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/RepositorySupport.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Spec.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionIsolation.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecBuilder.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/SpecContext.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/docs/README.md" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxDefinition.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateSpecContext.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxException.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/docs/README.md" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxManager.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcSpecContext.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxOutcome.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/AbstractWsRouter.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxResourceKey.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/Ws.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxStatus.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathWsRouterImpl.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TxSynchronization.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/websocket/WebSocketEndpoint.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/pom.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/websocket/WebSocketFrame.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/websocket/WebSocketHandler.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/websocket/WebSocketSession.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/HttpServerWebSocketTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/HibernateTxManagerTest.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/extension/FlashAppWebSocketTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/test/java/dev/relism/flash/ext/data/hibernate/TestHelper.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/extension/PackageScannerTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/pom.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/routing/AbstractWsRouterTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/WebSocketEndpointTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/WebSocketFrameTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxStatus.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/WebSocketSessionFrameTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/test/java/dev/relism/flash/ext/data/jdbc/JdbcTxManagerTest.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/WebSocketSessionTest.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteTarget.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/onlyws/OnlyWsEndpoint.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/FrontendType.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/scantest/ScanHttpHandler.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/Flash.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/websocket/scantest/ScanWsEndpoint.java" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/routers/radix/RadixPathRouterImpl.java" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/qodana.yaml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/encodings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/encodings.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/ext/jackson/JacksonExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/flash/ext/jackson/JacksonExtension.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/DataExtension.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/ext/jackson/JacksonMiddleware.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/flash/ext/jackson/JacksonMiddleware.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Repository.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/ext/jackson/Json.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/java/dev/relism/flash/ext/jackson/Json.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/ResourceRegistry.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/ext/jackson/JacksonExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonExtensionTest.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Sort.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/ext/jackson/JacksonMiddlewareTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JacksonMiddlewareTest.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/TransactionPropagation.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/ext/jackson/JsonTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/test/java/dev/relism/flash/ext/jackson/JsonTest.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-core/src/main/java/dev/relism/flash/ext/data/core/Tx.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/Bucket.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/Bucket.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateRepository.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/BucketStore.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/BucketStore.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxManager.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/Guard.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/Guard.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-hibernate/src/main/java/dev/relism/flash/ext/data/hibernate/HibernateTxStatus.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/KeyResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/KeyResolver.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcRepository.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/Limit.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/Limit.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxManager.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimitConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/LimitConfig.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-data-jdbc/src/main/java/dev/relism/flash/ext/data/jdbc/JdbcTxStatus.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimitStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/LimitStrategy.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiExtension.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimiterConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/LimiterConfig.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiExtensionTest.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/LimiterExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/LimiterExtension.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteExtensionTest.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/RateLimitStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/RateLimitStrategy.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/HttpServer.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/strategy/FixedWindowStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/strategy/FixedWindowStrategy.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/RequestParser.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/strategy/SlidingWindowStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/strategy/SlidingWindowStrategy.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/ServerHandle.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/ServerHandle.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/ext/limiter/strategy/TokenBucketStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java/dev/relism/flash/ext/limiter/strategy/TokenBucketStrategy.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashApp.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashApp.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/test/java/dev/relism/ext/limiter/LimiterOpenApiInteropTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/test/java/dev/relism/flash/ext/limiter/LimiterOpenApiInteropTest.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/Authenticated.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/Authenticated.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashRegistrar.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashRegistrar.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/ClaimsHolder.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/ClaimsHolder.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashScope.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashScope.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/ClientAuthMethod.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/DiscoveryClient.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/InMemoryOidcSessionStore.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/InMemoryOidcSessionStore.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/JwtUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/JwtValidator.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcAuthPolicy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcAuthPolicy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcMiddleware.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcMiddleware.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcProviderMetadata.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcSession.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSession.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcSessionStore.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSessionStore.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcStateStore.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcTokenResponse.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcUser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcUser.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/OidcValidationException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/PkceUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/RolesAllowed.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/RolesAllowed.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/ScopesAllowed.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/ScopesAllowed.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/ext/oidc/TokenClient.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcAuthPolicyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcAuthPolicyTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcMiddlewareAuthzTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcMiddlewareAuthzTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcOpenApiInteropTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/ext/oidc/OidcUserScopesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcUserScopesTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/APIResponse.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/APIResponse.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/APIResponses.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/APIResponses.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ApiOperation.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/ApiOperation.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ArraySchema.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/ArraySchema.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Content.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Content.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiBuilder.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiBuilder.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiContributor.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiContributor.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiContributorRegistry.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiContributorRegistry.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiOperationContribution.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiOperationContribution.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/OpenApiResponseContribution.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/OpenApiResponseContribution.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Parameter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Parameter.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/ParameterIn.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/ParameterIn.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Parameters.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Parameters.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/Schema.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/Schema.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/SchemaProperty.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/SchemaProperty.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/ext/openapi/SchemaType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java/dev/relism/flash/ext/openapi/SchemaType.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiBuilderTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiBuilderTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/ext/openapi/OpenApiExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/test/java/dev/relism/flash/ext/openapi/OpenApiExtensionTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/GraphSerializer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/GraphSerializer.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/RouteViewerDataHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/RouteViewerDataHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/RouteViewerExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/RouteViewerExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/RouteViewerHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/RouteViewerHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/RouteViewerStaticHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/RouteViewerStaticHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/model/RouteGraph.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/model/RouteGraph.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/model/RouteRecord.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/model/RouteRecord.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/ext/routeviewer/model/RouterNode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java/dev/relism/flash/ext/routeviewer/model/RouterNode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/BaseViewExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/BaseViewExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/BaseViewHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/BaseViewHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/GlobalValue.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/GlobalValue.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/RenderedView.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/RenderedView.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/ViewModel.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/ViewModel.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/ext/view/core/ViewRuntimeBridge.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/main/java/dev/relism/flash/ext/view/core/ViewRuntimeBridge.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/test/java/dev/relism/ext/view/core/ViewModelTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-core/src/test/java/dev/relism/flash/ext/view/core/ViewModelTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/README.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/docs/handlers.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/docs/handlers.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.class" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.class" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteRuntime.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteRuntime.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteSettings.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteSettings.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteTarget.java" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/JteTargetResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteTargetResolver.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/ext/view/jte/Template.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/Template.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/ext/view/jte/JteExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteExtensionTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/ext/view/jte/JteRuntimeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteRuntimeTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/ext/view/jte/JteSettingsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteSettingsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/ext/view/jte/JteTargetResolverTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/JteTargetResolverTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/ext/view/jte/model/HomePage.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/java/dev/relism/flash/ext/view/jte/model/HomePage.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/resources/templates/pages/home.jte" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-jte/src/test/resources/templates/pages/home.jte" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/README.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/docs/fragments.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/docs/fragments.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/docs/handlers.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/docs/handlers.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/Fragment.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/Fragment.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/Template.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/Template.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafRuntime.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafRuntime.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafSettings.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafSettings.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafTarget.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafTarget.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/ext/view/thymeleaf/ThymeleafTargetResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/main/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafTargetResolver.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/ext/view/thymeleaf/ThymeleafExtensionTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafExtensionTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/ext/view/thymeleaf/ThymeleafRuntimeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafRuntimeTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/ext/view/thymeleaf/ThymeleafSettingsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafSettingsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/ext/view/thymeleaf/ThymeleafTargetResolverTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-view-thymeleaf/src/test/java/dev/relism/flash/ext/view/thymeleaf/ThymeleafTargetResolverTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/README.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/asset-sources.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/asset-sources.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/configuration.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/configuration.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/dev-lifecycle.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/dev-lifecycle.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/frontend-selection.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/frontend-selection.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/modes.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/modes.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/package-managers.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/package-managers.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/performance.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/performance.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/prod-serving.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/prod-serving.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/routing-fallback.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/routing-fallback.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/docs/security-policies.md" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/docs/security-policies.md" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/pom.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetCatalog.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetCatalog.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetEntry.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetEntry.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetIo.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetIo.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetLoadRequest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetLoadRequest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetMetadata.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetMetadata.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetPaths.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetPaths.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetsSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetsSource.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/AssetsSources.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/AssetsSources.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/BasePathEnforcementMode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/BasePathEnforcementMode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/ClasspathAssetManifest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/ClasspathAssetManifest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/ClasspathAssetsSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/ClasspathAssetsSource.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/CommandOrchestrator.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/CommandOrchestrator.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/CommandSafetyMode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/CommandSafetyMode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/CommandSafetyPolicy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/CommandSafetyPolicy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/CommandTokens.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/CommandTokens.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/FilesystemAssetsSource.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/FilesystemAssetsSource.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/FrontendStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/FrontendStrategy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/FrontendType.java" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/FrontendTypeResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/FrontendTypeResolver.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/InstallCache.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/InstallCache.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/InstallPolicy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/InstallPolicy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/LoggingMode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/LoggingMode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/MimeTypes.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/MimeTypes.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/ModeResolver.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/ModeResolver.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/OperationMode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/OperationMode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/PackageManager.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/PackageManager.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/PackageManagerAdapter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/PackageManagerAdapter.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/RuntimeEnvironment.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/RuntimeEnvironment.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/RuntimeMode.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/RuntimeMode.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/SpaFallbackPolicy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/SpaFallbackPolicy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/StaticAssetServingPolicy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/StaticAssetServingPolicy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/ViteFrontendStrategy.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/ViteFrontendStrategy.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/WatchList.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/WatchList.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/WebBundlerConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/WebBundlerConfig.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/WebBundlerExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/WebBundlerExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/main/java/dev/relism/ext/webbundler/WebBundlerRuntime.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/main/java/dev/relism/flash/ext/webbundler/WebBundlerRuntime.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/AssetsSourceTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/AssetsSourceTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/CommandSafetyPolicyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/CommandSafetyPolicyTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/InstallCacheTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/InstallCacheTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/ModeResolverTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/ModeResolverTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/PackageManagerAdapterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/PackageManagerAdapterTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerConfigTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerConfigTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionDevGuardTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionDevGuardTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/flash-web-bundler/src/test/java/dev/relism/ext/webbundler/WebBundlerExtensionIntegrationTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/flash-ext-web-bundler/src/test/java/dev/relism/flash/ext/webbundler/WebBundlerExtensionIntegrationTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash-extensions/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-extensions/pom.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/ChunkedInputStream.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/ChunkedInputStream.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Flash.java" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/HttpServer.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/RequestParser.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/ServerHandle.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/ServerHandle.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/api/multipart/Multipart.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/api/multipart/Multipart.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/api/multipart/Part.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/api/multipart/Part.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/exceptions/HttpException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/exceptions/HttpException.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/exceptions/InitializationException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/exceptions/InitializationException.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/AnnotationProcessor.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/AnnotationProcessor.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/ExtensionPhase.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/ExtensionPhase.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashApp.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashApp.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashConfiguration.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashContext.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashContext.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashExtension.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashExtension.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashRegistrar.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashRegistrar.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/FlashScope.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/FlashScope.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/PackageScanner.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/PackageScanner.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/RouteDefinition.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/RouteDefinition.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/RouteEvent.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/RouteEvent.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/extension/RouteListener.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/extension/RouteListener.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/http/ContentType.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpMethod.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/http/HttpMethod.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/http/HttpStatus.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/HeaderMap.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/PathParams.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/QueryParams.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/Request.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestBody.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/RequestBody.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/RequestHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHelper.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/RequestHelper.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestLine.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/RequestLine.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/Response.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/models/SimpleHandler.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/AbstractRouter.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/CONNECT.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/CONNECT.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/DELETE.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/DELETE.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GET.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/GET.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/HEAD.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/HEAD.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Middleware.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/Middleware.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/OPTIONS.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/OPTIONS.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PATCH.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/PATCH.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/POST.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/POST.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PURGE.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/PURGE.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PUT.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/PUT.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PathUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/PathUtils.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/Route.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Routes.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/Routes.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/TRACE.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/TRACE.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ByteTemplate.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/template/ByteTemplate.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ErrorPages.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/flash/template/ErrorPages.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/ChunkedInputStreamTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/ChunkedInputStreamTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/HttpServerConcurrencyTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/HttpServerTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/RequestParserTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/api/multipart/MultipartTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/api/multipart/MultipartTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/http/ContentTypeTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpMethodTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/http/HttpMethodTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/http/HttpStatusTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/HeaderMapTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/PathParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/PathParamsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/QueryParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/QueryParamsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestBodyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/RequestBodyTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/RequestLineTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/RequestTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/ResponseTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/SimpleHandlerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/models/SimpleHandlerTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/AbstractRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/routing/AbstractRouterTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/PathUtilsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/routing/PathUtilsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathRouterImplTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/routing/routers/fastpathrouter/FastPathViewsTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ByteTemplateTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/template/ByteTemplateTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/flash/template/ErrorPagesTest.java" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="CopilotChats">
|
||||||
|
<option name="panelChat">
|
||||||
|
<chat>
|
||||||
|
<option name="activeSessionId" value="82f48e09-2ca4-4a27-9a04-c65d7a5f0b88" />
|
||||||
|
<option name="sessions">
|
||||||
|
<session>
|
||||||
|
<option name="chatType" value="PANEL" />
|
||||||
|
<option name="id" value="a8d77f48-1eb9-4c5a-961c-1e1452a25ca0" />
|
||||||
|
<option name="modeId" value="Agent" />
|
||||||
|
<option name="modelType" value="builtin_family" />
|
||||||
|
<option name="modelValue" value="auto" />
|
||||||
|
<option name="status" value="Completed" />
|
||||||
|
<option name="targetType" value="LOCAL" />
|
||||||
|
</session>
|
||||||
|
<session>
|
||||||
|
<option name="chatType" value="PANEL" />
|
||||||
|
<option name="id" value="82f48e09-2ca4-4a27-9a04-c65d7a5f0b88" />
|
||||||
|
<option name="modeId" value="Agent" />
|
||||||
|
<option name="modelType" value="builtin_family" />
|
||||||
|
<option name="modelValue" value="auto" />
|
||||||
|
<option name="targetType" value="LOCAL" />
|
||||||
|
</session>
|
||||||
|
</option>
|
||||||
|
<option name="type" value="PANEL" />
|
||||||
|
</chat>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="CopilotPersistence">
|
<component name="CopilotPersistence">
|
||||||
<persistenceIdMap>
|
<persistenceIdMap>
|
||||||
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/Flash" value="3Axc6mzLyNvh4TgGFvNSCSq83xw" />
|
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/Flash" value="3Axc6mzLyNvh4TgGFvNSCSq83xw" />
|
||||||
@@ -298,7 +101,7 @@
|
|||||||
</persistenceIdMap>
|
</persistenceIdMap>
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="407" />
|
<option name="cachedIndexableFilesCount" value="440" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="FileTemplateManagerImpl">
|
<component name="FileTemplateManagerImpl">
|
||||||
@@ -341,64 +144,64 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"Application.(dev) flash-bench.executor": "Run",
|
"Application.(dev) flash-bench.executor": "Run",
|
||||||
"Application.ExternalBenchmark (1).executor": "Run",
|
"Application.ExternalBenchmark (1).executor": "Run",
|
||||||
"Application.ExternalBenchmark.executor": "Run",
|
"Application.ExternalBenchmark.executor": "Run",
|
||||||
"Application.Main.executor": "Run",
|
"Application.Main.executor": "Run",
|
||||||
"Application.MainAlt.executor": "Run",
|
"Application.MainAlt.executor": "Run",
|
||||||
"Application.dev.relism.bench.Main.executor": "Run",
|
"Application.dev.relism.bench.Main.executor": "Run",
|
||||||
"JUnit.RequestParserTest.executor": "Run",
|
"JUnit.RequestParserTest.executor": "Run",
|
||||||
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
||||||
"Maven.FlashPractice [test].executor": "Run",
|
"Maven.FlashPractice [test].executor": "Run",
|
||||||
"Maven.flash [compile].executor": "Run",
|
"Maven.flash [compile].executor": "Run",
|
||||||
"Maven.flash [install].executor": "Run",
|
"Maven.flash [install].executor": "Run",
|
||||||
"Maven.flash [test].executor": "Run",
|
"Maven.flash [test].executor": "Run",
|
||||||
"Maven.flash [verify].executor": "Run",
|
"Maven.flash [verify].executor": "Run",
|
||||||
"Maven.flash-bench [clean].executor": "Run",
|
"Maven.flash-bench [clean].executor": "Run",
|
||||||
"Maven.flash-bench [install].executor": "Run",
|
"Maven.flash-bench [install].executor": "Run",
|
||||||
"Maven.flash-bench [package].executor": "Run",
|
"Maven.flash-bench [package].executor": "Run",
|
||||||
"Maven.flash-bench [validate].executor": "Run",
|
"Maven.flash-bench [validate].executor": "Run",
|
||||||
"Maven.flash-ext-limiter [install].executor": "Run",
|
"Maven.flash-ext-limiter [install].executor": "Run",
|
||||||
"Maven.flash-ext-limiter [package].executor": "Run",
|
"Maven.flash-ext-limiter [package].executor": "Run",
|
||||||
"Maven.flash-ext-view [verify].executor": "Run",
|
"Maven.flash-ext-view [verify].executor": "Run",
|
||||||
"Maven.flash-parent [clean].executor": "Run",
|
"Maven.flash-parent [clean].executor": "Run",
|
||||||
"Maven.flash-parent [compile].executor": "Run",
|
"Maven.flash-parent [compile].executor": "Run",
|
||||||
"Maven.flash-parent [deploy].executor": "Run",
|
"Maven.flash-parent [deploy].executor": "Run",
|
||||||
"Maven.flash-parent [install].executor": "Run",
|
"Maven.flash-parent [install].executor": "Run",
|
||||||
"Maven.flash-parent [package].executor": "Run",
|
"Maven.flash-parent [package].executor": "Run",
|
||||||
"Maven.flash-parent [test].executor": "Run",
|
"Maven.flash-parent [test].executor": "Run",
|
||||||
"Maven.flash-parent [validate].executor": "Run",
|
"Maven.flash-parent [validate].executor": "Run",
|
||||||
"Maven.flash-parent [verify].executor": "Run",
|
"Maven.flash-parent [verify].executor": "Run",
|
||||||
"Maven.flash-web-bundler [test].executor": "Run",
|
"Maven.flash-web-bundler [test].executor": "Run",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||||
"git-widget-placeholder": "master",
|
"git-widget-placeholder": "master",
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
"kotlin-language-version-configured": "true",
|
"kotlin-language-version-configured": "true",
|
||||||
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"npm.build.executor": "Run",
|
"npm.build.executor": "Run",
|
||||||
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash/flash-extensions/flash-ext-data/src/main/java/dev/relism/Main.java",
|
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash/flash-extensions/flash-ext-data/src/main/java/dev/relism/Main.java",
|
||||||
"project.structure.last.edited": "Modules",
|
"project.structure.last.edited": "Modules",
|
||||||
"project.structure.proportion": "0.15",
|
"project.structure.proportion": "0.15",
|
||||||
"project.structure.side.proportion": "0.1150748",
|
"project.structure.side.proportion": "0.1150748",
|
||||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
|
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings",
|
||||||
"ts.external.directory.path": "C:\\Users\\elorc\\Documents\\Coding\\Java\\practice\\Flash\\nuxt-shadcn-dashboard\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "C:\\Users\\elorc\\Documents\\Coding\\Java\\practice\\Flash\\nuxt-shadcn-dashboard\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
|
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
|
||||||
@@ -534,7 +337,16 @@
|
|||||||
<workItem from="1776931805422" duration="16122000" />
|
<workItem from="1776931805422" duration="16122000" />
|
||||||
<workItem from="1777051880577" duration="2650000" />
|
<workItem from="1777051880577" duration="2650000" />
|
||||||
<workItem from="1777150747725" duration="837000" />
|
<workItem from="1777150747725" duration="837000" />
|
||||||
<workItem from="1777198150359" duration="638000" />
|
<workItem from="1777198150359" duration="5628000" />
|
||||||
|
<workItem from="1777451402985" duration="709000" />
|
||||||
|
<workItem from="1777534738187" duration="13411000" />
|
||||||
|
<workItem from="1777970837797" duration="5042000" />
|
||||||
|
<workItem from="1778160998498" duration="2931000" />
|
||||||
|
<workItem from="1778319397472" duration="5040000" />
|
||||||
|
<workItem from="1778352978922" duration="2169000" />
|
||||||
|
<workItem from="1778414714349" duration="23000" />
|
||||||
|
<workItem from="1778417425077" duration="3241000" />
|
||||||
|
<workItem from="1778489168036" duration="7070000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Initial">
|
<task id="LOCAL-00001" summary="Initial">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -632,7 +444,23 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1776724331443</updated>
|
<updated>1776724331443</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="13" />
|
<task id="LOCAL-00013" summary="refactor: rename packages and files to use 'flash' prefix for consistency">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1777199691803</created>
|
||||||
|
<option name="number" value="00013" />
|
||||||
|
<option name="presentableId" value="LOCAL-00013" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1777199691804</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00014" summary="fix: enhance global key validation and update template parameters">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1777451475753</created>
|
||||||
|
<option name="number" value="00014" />
|
||||||
|
<option name="presentableId" value="LOCAL-00014" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1777451475753</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="15" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -674,7 +502,11 @@
|
|||||||
<MESSAGE value="preparing for another refactoring..." />
|
<MESSAGE value="preparing for another refactoring..." />
|
||||||
<MESSAGE value="implement OpenAPI contributor integration for rate limiting and response headers" />
|
<MESSAGE value="implement OpenAPI contributor integration for rate limiting and response headers" />
|
||||||
<MESSAGE value="add core view extension with JTE and Thymeleaf support" />
|
<MESSAGE value="add core view extension with JTE and Thymeleaf support" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="add core view extension with JTE and Thymeleaf support" />
|
<MESSAGE value="refactor: rename packages and files to use 'flash' prefix for consistency" />
|
||||||
|
<MESSAGE value="fix: enhance global key validation and update template parameters" />
|
||||||
|
<MESSAGE value="chore(release): prepare 2.1.0-SNAPSHOT" />
|
||||||
|
<MESSAGE value="feat: introduce Spec and Query interfaces with transaction propagation enhancements" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="feat: introduce Spec and Query interfaces with transaction propagation enhancements" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
config:
|
||||||
|
target: "ws://localhost:8080/echo"
|
||||||
|
engines:
|
||||||
|
ws: {}
|
||||||
|
phases:
|
||||||
|
- duration: 30
|
||||||
|
arrivalRate: 50
|
||||||
|
rampTo: 500
|
||||||
|
name: "Riscaldamento progressivo"
|
||||||
|
- duration: 120
|
||||||
|
arrivalRate: 1000 # 1000 nuovi utenti al secondo
|
||||||
|
name: "Carico Estremo"
|
||||||
|
ensure:
|
||||||
|
maxErrorRate: 5
|
||||||
|
p99: 150
|
||||||
|
|
||||||
|
scenarios:
|
||||||
|
- name: "Saturazione Totale"
|
||||||
|
engine: ws
|
||||||
|
flow:
|
||||||
|
- loop:
|
||||||
|
- send: "Benchmark data"
|
||||||
|
# Rimosso il 'think' per eliminare il limite artificiale di 10msg/s per utente
|
||||||
|
count: 100 # Ogni utente spara a raffica 100 messaggi senza pause
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# flash-ext-data-core
|
||||||
|
|
||||||
|
Core comune per il layer dati di Flash.
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Questo modulo definisce il contratto transazionale condiviso tra le implementazioni backend.
|
||||||
|
Non parla con Hibernate o JDBC direttamente: espone solo astrazioni e un runtime minimale.
|
||||||
|
|
||||||
|
## Componenti
|
||||||
|
|
||||||
|
- `TxDefinition`: metadata immutabile della transazione.
|
||||||
|
- `TxStatus`: stato runtime restituito dal manager.
|
||||||
|
- `TxManager`: contratto per `begin`, `commit`, `rollback`.
|
||||||
|
- `Tx`: orchestration runtime e stack transazionale per thread.
|
||||||
|
- `ResourceRegistry`: storage thread-local di risorse e synchronizations.
|
||||||
|
- `Repository<T, ID>`: base repository auto-transazionale.
|
||||||
|
- `Spec<T>`: predicato componibile.
|
||||||
|
- `Query<T>`: oggetto query con spec, sort e paging.
|
||||||
|
- `SpecBuilder<T>`: DSL fluente per costruire spec tipizzate.
|
||||||
|
- `RepositorySupport<T, ID>`: helper interno condiviso.
|
||||||
|
- `TransactionPropagation`: semantica di propagazione.
|
||||||
|
- `TransactionIsolation`: livello di isolamento.
|
||||||
|
- `TxSynchronization`: hook lifecycle.
|
||||||
|
|
||||||
|
## Modello di esecuzione
|
||||||
|
|
||||||
|
Il flusso è:
|
||||||
|
|
||||||
|
1. `Tx.call(definition, work)` chiama `TxManager.begin(definition)`.
|
||||||
|
2. Il `TxManager` crea un `TxStatus` backend-specific.
|
||||||
|
3. Lo status viene pushato nello stack thread-local.
|
||||||
|
4. Il lavoro usa `Tx.resource(Class)` per ottenere la risorsa corrente.
|
||||||
|
5. A fine lavoro `Tx` decide tra `commit` e `rollback`.
|
||||||
|
6. Lo stack viene poppato e il thread-local viene pulito se vuoto.
|
||||||
|
|
||||||
|
## Propagation supportata
|
||||||
|
|
||||||
|
- `REQUIRED`: usa la tx attiva oppure ne apre una nuova.
|
||||||
|
- `REQUIRES_NEW`: sospende la tx corrente e apre una nuova tx.
|
||||||
|
- `SUPPORTS`: se esiste una tx attiva si aggancia, altrimenti esegue senza tx.
|
||||||
|
- `NOT_SUPPORTED`: sospende la tx corrente ed esegue senza tx.
|
||||||
|
- `MANDATORY`: richiede una tx attiva.
|
||||||
|
|
||||||
|
## Uso di `Repository`
|
||||||
|
|
||||||
|
`Repository` è la base comune per le repository concrete.
|
||||||
|
Ogni operazione pubblica usa internamente una tx `REQUIRED` o `REQUIRED` read-only.
|
||||||
|
|
||||||
|
Le sottoclassi implementano i metodi `doXxx(...)` del nuovo modello:
|
||||||
|
|
||||||
|
- `doFind(Query<T>)`
|
||||||
|
- `doFindOne(Spec<T>)`
|
||||||
|
- `doFindPage(Query<T>)`
|
||||||
|
- `doDeleteAll(Spec<T>)`
|
||||||
|
- `doUpdateAll(Spec<T>, T)`
|
||||||
|
|
||||||
|
I vecchi overload di `findAll(...)` e `findPage(...)` sono stati ridotti a una combinazione di `Query<T>` e `Spec<T>`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public abstract class Repository<T, ID> {
|
||||||
|
protected Repository(Tx tx) { ... }
|
||||||
|
protected final <R> R tx(Tx.TxCallable<R> work) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composizione con Flash
|
||||||
|
|
||||||
|
`DataExtension` registra:
|
||||||
|
|
||||||
|
- `Tx` nel `FlashContext`
|
||||||
|
- `TxManager` nel `FlashContext`
|
||||||
|
- un annotation processor per `@Transactional`
|
||||||
|
|
||||||
|
Questo rende il layer dati componibile con il sistema di extension di Flash senza stato globale.
|
||||||
|
|
||||||
|
## Note implementative
|
||||||
|
|
||||||
|
- Lo stack transazionale è thread-local e viene ripulito quando torna vuoto.
|
||||||
|
- Le risorse backend sono sospese e ripristinate per `REQUIRES_NEW` e `NOT_SUPPORTED`.
|
||||||
|
- `TxSynchronization` è il punto di aggancio per hook di commit/rollback/completion.
|
||||||
+6
-4
@@ -7,7 +7,6 @@ import dev.relism.flash.ext.data.core.TransactionPropagation;
|
|||||||
import dev.relism.flash.extension.ExtensionPhase;
|
import dev.relism.flash.extension.ExtensionPhase;
|
||||||
import dev.relism.flash.extension.FlashContext;
|
import dev.relism.flash.extension.FlashContext;
|
||||||
import dev.relism.flash.extension.FlashExtension;
|
import dev.relism.flash.extension.FlashExtension;
|
||||||
import dev.relism.flash.extension.FlashRegistrar;
|
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
@@ -16,14 +15,16 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public final class DataExtension implements FlashExtension {
|
public final class DataExtension implements FlashExtension {
|
||||||
private final TxManager txManager;
|
private final TxManager txManager;
|
||||||
|
private final Tx tx;
|
||||||
|
|
||||||
public DataExtension(TxManager txManager) {
|
public DataExtension(TxManager txManager) {
|
||||||
this.txManager = Objects.requireNonNull(txManager);
|
this.txManager = Objects.requireNonNull(txManager);
|
||||||
|
this.tx = new Tx(txManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void provide(FlashContext ctx) {
|
public void provide(FlashContext ctx) {
|
||||||
Tx.init(txManager);
|
ctx.provide(Tx.class, tx);
|
||||||
ctx.provide(TxManager.class, txManager);
|
ctx.provide(TxManager.class, txManager);
|
||||||
ctx.addAnnotationProcessor(handlerClass -> {
|
ctx.addAnnotationProcessor(handlerClass -> {
|
||||||
Transactional ann = handlerClass.getAnnotation(Transactional.class);
|
Transactional ann = handlerClass.getAnnotation(Transactional.class);
|
||||||
@@ -33,7 +34,7 @@ public final class DataExtension implements FlashExtension {
|
|||||||
TxDefinition definition = TxDefinition.DEFAULTS
|
TxDefinition definition = TxDefinition.DEFAULTS
|
||||||
.withPropagation(mapTxType(ann.value()));
|
.withPropagation(mapTxType(ann.value()));
|
||||||
Middleware middleware = next -> (req, res) -> {
|
Middleware middleware = next -> (req, res) -> {
|
||||||
return Tx.call(definition, () -> next.handle(req, res));
|
return tx.call(definition, () -> next.handle(req, res));
|
||||||
};
|
};
|
||||||
return List.of(middleware);
|
return List.of(middleware);
|
||||||
});
|
});
|
||||||
@@ -46,8 +47,9 @@ public final class DataExtension implements FlashExtension {
|
|||||||
|
|
||||||
private TransactionPropagation mapTxType(Transactional.TxType txType) {
|
private TransactionPropagation mapTxType(Transactional.TxType txType) {
|
||||||
return switch (txType) {
|
return switch (txType) {
|
||||||
case REQUIRED, SUPPORTS -> TransactionPropagation.REQUIRED;
|
case REQUIRED -> TransactionPropagation.REQUIRED;
|
||||||
case REQUIRES_NEW -> TransactionPropagation.REQUIRES_NEW;
|
case REQUIRES_NEW -> TransactionPropagation.REQUIRES_NEW;
|
||||||
|
case SUPPORTS -> TransactionPropagation.SUPPORTS;
|
||||||
case MANDATORY -> TransactionPropagation.MANDATORY;
|
case MANDATORY -> TransactionPropagation.MANDATORY;
|
||||||
case NOT_SUPPORTED, NEVER -> TransactionPropagation.NOT_SUPPORTED;
|
case NOT_SUPPORTED, NEVER -> TransactionPropagation.NOT_SUPPORTED;
|
||||||
};
|
};
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
|
public record Query<T>(Spec<T> spec, Sort sort, Integer page, Integer size) {
|
||||||
|
public Query {
|
||||||
|
spec = spec == null ? Spec.all() : spec;
|
||||||
|
sort = sort == null ? Sort.unsorted() : sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Query<T> all() {
|
||||||
|
return new Query<>(Spec.all(), Sort.unsorted(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query<T> where(Spec<T> spec) {
|
||||||
|
return new Query<>(spec, sort, page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query<T> orderBy(Sort sort) {
|
||||||
|
return new Query<>(spec, sort, page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query<T> page(int page, int size) {
|
||||||
|
return new Query<>(spec, sort, page, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPaged() {
|
||||||
|
return page != null && size != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
-91
@@ -4,109 +4,118 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
public abstract class Repository<T, ID> extends RepositorySupport<T, ID> {
|
||||||
* Base repository. Subclasses only extend this — never HibernateRepository
|
|
||||||
* or JdbcRepository directly. The concrete backing is transparent.
|
|
||||||
*
|
|
||||||
* Every method auto-wraps in REQUIRED transaction — safe to call with or
|
|
||||||
* without an active transaction on the thread.
|
|
||||||
*/
|
|
||||||
public abstract class Repository<T, ID> {
|
|
||||||
|
|
||||||
private final TxDefinition required = TxDefinition.DEFAULTS
|
protected Repository(Tx tx) {
|
||||||
.withPropagation(TransactionPropagation.REQUIRED);
|
super(tx);
|
||||||
|
}
|
||||||
// ── CRUD ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public Optional<T> findById(ID id) {
|
public Optional<T> findById(ID id) {
|
||||||
return tx(() -> doFindById(id));
|
return roQuery(() -> doFindById(id));
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> findAll() {
|
|
||||||
return tx(this::doFindAll);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> findAll(int page, int size) {
|
|
||||||
return tx(() -> doFindAll(page, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> findAll(Sort sort) {
|
|
||||||
return tx(() -> doFindAll(sort));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> findAll(int page, int size, Sort sort) {
|
|
||||||
return tx(() -> doFindAll(page, size, sort));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Page<T> findPage(int page, int size) {
|
|
||||||
return tx(() -> doFindPage(page, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Page<T> findPage(int page, int size, Sort sort) {
|
|
||||||
return tx(() -> doFindPage(page, size, sort));
|
|
||||||
}
|
|
||||||
|
|
||||||
public T save(T entity) {
|
|
||||||
return tx(() -> doSave(entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> saveAll(Iterable<T> entities) {
|
|
||||||
return tx(() -> {
|
|
||||||
List<T> saved = new ArrayList<>();
|
|
||||||
for (T e : entities) saved.add(doSave(e));
|
|
||||||
return saved;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public T update(T entity) {
|
|
||||||
return tx(() -> doUpdate(entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(T entity) {
|
|
||||||
tx(() -> { doDelete(entity); return null; });
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteById(ID id) {
|
|
||||||
tx(() -> { doDeleteById(id); return null; });
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteAll(Iterable<T> entities) {
|
|
||||||
tx(() -> { entities.forEach(this::doDelete); return null; });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean existsById(ID id) {
|
public boolean existsById(ID id) {
|
||||||
return tx(() -> doExistsById(id));
|
return roQuery(() -> doExistsById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public long count() {
|
public long count() {
|
||||||
return tx(this::doCount);
|
return roQuery(this::doCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-wrap helper ──────────────────────────────────────────────────────
|
public List<T> findAll() {
|
||||||
|
return findAll(Query.all());
|
||||||
/**
|
|
||||||
* Ensures the work runs inside a transaction.
|
|
||||||
* If one is already active (caller annotated @Transactional or inside Tx.run)
|
|
||||||
* it joins it — no new connection opened.
|
|
||||||
* If none is active it opens one, commits, and closes it transparently.
|
|
||||||
*/
|
|
||||||
protected final <R> R tx(Tx.TxCallable<R> work) {
|
|
||||||
return Tx.call(required, work);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Abstract — implemented by HibernateRepository / JdbcRepository ────────
|
public List<T> findAll(Spec<T> spec) {
|
||||||
|
return findAll(Query.<T>all().where(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> findAll(Query<T> query) {
|
||||||
|
return roQuery(() -> doFind(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<T> findPage(Query<T> query) {
|
||||||
|
return roQuery(() -> doFindPage(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<T> findOne(Spec<T> spec) {
|
||||||
|
return roQuery(() -> doFindOne(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
public T save(T entity) {
|
||||||
|
return rwQuery(() -> doSave(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public T update(T entity) {
|
||||||
|
return rwQuery(() -> doUpdate(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> saveAll(Iterable<T> entities) {
|
||||||
|
return rwQuery(() -> doSaveAll(entities));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(T entity) {
|
||||||
|
rwQuery(() -> {
|
||||||
|
doDelete(entity);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteById(ID id) {
|
||||||
|
rwQuery(() -> {
|
||||||
|
doDeleteById(id);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int deleteAll(Spec<T> spec) {
|
||||||
|
return rwQuery(() -> doDeleteAll(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateAll(Spec<T> spec, T patch) {
|
||||||
|
return rwQuery(() -> doUpdateAll(spec, patch));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> findAll(int page, int size) {
|
||||||
|
return findAll(Query.<T>all().page(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> findAll(Sort sort) {
|
||||||
|
return findAll(Query.<T>all().orderBy(sort));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> findAll(int page, int size, Sort sort) {
|
||||||
|
return findAll(Query.<T>all().orderBy(sort).page(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<T> findPage(int page, int size) {
|
||||||
|
return findPage(Query.<T>all().page(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<T> findPage(int page, int size, Sort sort) {
|
||||||
|
return findPage(Query.<T>all().orderBy(sort).page(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAll(Iterable<T> entities) {
|
||||||
|
rwQuery(() -> {
|
||||||
|
for (T entity : entities) {
|
||||||
|
doDelete(entity);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract Optional<T> doFindById(ID id);
|
protected abstract Optional<T> doFindById(ID id);
|
||||||
protected abstract List<T> doFindAll();
|
protected abstract List<T> doFind(Query<T> query);
|
||||||
protected abstract List<T> doFindAll(int page, int size);
|
protected abstract Optional<T> doFindOne(Spec<T> spec);
|
||||||
protected abstract List<T> doFindAll(Sort sort);
|
protected abstract Page<T> doFindPage(Query<T> query);
|
||||||
protected abstract List<T> doFindAll(int page, int size, Sort sort);
|
protected abstract boolean doExistsById(ID id);
|
||||||
protected abstract Page<T> doFindPage(int page, int size);
|
protected abstract long doCount();
|
||||||
protected abstract Page<T> doFindPage(int page, int size, Sort sort);
|
protected abstract T doSave(T entity);
|
||||||
protected abstract T doSave(T entity);
|
protected abstract List<T> doSaveAll(Iterable<T> entities);
|
||||||
protected abstract T doUpdate(T entity);
|
protected abstract T doUpdate(T entity);
|
||||||
protected abstract void doDelete(T entity);
|
protected abstract void doDelete(T entity);
|
||||||
protected abstract void doDeleteById(ID id);
|
protected abstract void doDeleteById(ID id);
|
||||||
protected abstract boolean doExistsById(ID id);
|
protected abstract int doDeleteAll(Spec<T> spec);
|
||||||
protected abstract long doCount();
|
protected abstract int doUpdateAll(Spec<T> spec, T patch);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
|
public abstract class RepositorySupport<T, ID> {
|
||||||
|
private final Tx tx;
|
||||||
|
private final TxDefinition rw = TxDefinition.DEFAULTS
|
||||||
|
.withPropagation(TransactionPropagation.REQUIRED);
|
||||||
|
private final TxDefinition ro = rw.asReadOnly();
|
||||||
|
|
||||||
|
protected RepositorySupport(Tx tx) {
|
||||||
|
this.tx = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Tx tx() {
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <R> R roQuery(Tx.TxCallable<R> work) {
|
||||||
|
return tx.call(ro, work);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <R> R rwQuery(Tx.TxCallable<R> work) {
|
||||||
|
return tx.call(rw, work);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -31,6 +31,11 @@ public final class ResourceRegistry {
|
|||||||
SYNCHRONIZATIONS.get().clear();
|
SYNCHRONIZATIONS.get().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void cleanup() {
|
||||||
|
RESOURCES.remove();
|
||||||
|
SYNCHRONIZATIONS.remove();
|
||||||
|
}
|
||||||
|
|
||||||
public static <R> R get(TxResourceKey key, Class<R> type) {
|
public static <R> R get(TxResourceKey key, Class<R> type) {
|
||||||
Object value = RESOURCES.get().get(key);
|
Object value = RESOURCES.get().get(key);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
|||||||
+5
-1
@@ -7,6 +7,10 @@ public record Sort(List<Column> columns) {
|
|||||||
|
|
||||||
public record Column(String column, boolean asc) {}
|
public record Column(String column, boolean asc) {}
|
||||||
|
|
||||||
|
public static Sort unsorted() { return new Sort(List.of()); }
|
||||||
|
|
||||||
|
public boolean isSorted() { return !columns.isEmpty(); }
|
||||||
|
|
||||||
public static Sort by(String column) { return new Sort(List.of(new Column(column, true))); }
|
public static Sort by(String column) { return new Sort(List.of(new Column(column, true))); }
|
||||||
public static Sort desc(String column) { return new Sort(List.of(new Column(column, false))); }
|
public static Sort desc(String column) { return new Sort(List.of(new Column(column, false))); }
|
||||||
public static Sort by(String col, boolean asc){ return new Sort(List.of(new Column(col, asc))); }
|
public static Sort by(String col, boolean asc){ return new Sort(List.of(new Column(col, asc))); }
|
||||||
@@ -19,4 +23,4 @@ public record Sort(List<Column> columns) {
|
|||||||
next.add(new Column(column, asc));
|
next.add(new Column(column, asc));
|
||||||
return new Sort(next);
|
return new Sort(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Spec<T> {
|
||||||
|
String toFragment(SpecContext ctx);
|
||||||
|
|
||||||
|
default Spec<T> and(Spec<T> other) {
|
||||||
|
return ctx -> "(" + this.toFragment(ctx) + " AND " + other.toFragment(ctx) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
default Spec<T> or(Spec<T> other) {
|
||||||
|
return ctx -> "(" + this.toFragment(ctx) + " OR " + other.toFragment(ctx) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
default Spec<T> not() {
|
||||||
|
return ctx -> "NOT (" + this.toFragment(ctx) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> Spec<T> all() {
|
||||||
|
return ctx -> "1=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> Spec<T> none() {
|
||||||
|
return ctx -> "1=0";
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class SpecBuilder<T> {
|
||||||
|
private SpecBuilder() {}
|
||||||
|
|
||||||
|
public static <T, V> FieldSpec<T, V> field(String column) {
|
||||||
|
return new FieldSpec<>(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class FieldSpec<T, V> {
|
||||||
|
private final String column;
|
||||||
|
|
||||||
|
private FieldSpec(String column) {
|
||||||
|
this.column = Objects.requireNonNull(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Spec<T> eq(V value) { return ctx -> column + " = " + ctx.bind(value); }
|
||||||
|
public Spec<T> neq(V value) { return ctx -> column + " != " + ctx.bind(value); }
|
||||||
|
public Spec<T> like(String pattern) { return ctx -> column + " like " + ctx.bind(pattern); }
|
||||||
|
public Spec<T> isNull() { return ctx -> column + " is null"; }
|
||||||
|
public Spec<T> isNotNull() { return ctx -> column + " is not null"; }
|
||||||
|
|
||||||
|
public Spec<T> in(Collection<V> values) {
|
||||||
|
return ctx -> column + " in (" + values.stream().map(ctx::bind).collect(Collectors.joining(", ")) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public <C extends Comparable<C>> Spec<T> gt(C value) { return ctx -> column + " > " + ctx.bind(value); }
|
||||||
|
public <C extends Comparable<C>> Spec<T> lt(C value) { return ctx -> column + " < " + ctx.bind(value); }
|
||||||
|
public <C extends Comparable<C>> Spec<T> between(C lo, C hi) {
|
||||||
|
return ctx -> column + " between " + ctx.bind(lo) + " and " + ctx.bind(hi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.relism.flash.ext.data.core;
|
||||||
|
|
||||||
|
public interface SpecContext {
|
||||||
|
String bind(Object value);
|
||||||
|
}
|
||||||
+1
@@ -3,6 +3,7 @@ package dev.relism.flash.ext.data.core;
|
|||||||
public enum TransactionPropagation {
|
public enum TransactionPropagation {
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
REQUIRES_NEW,
|
REQUIRES_NEW,
|
||||||
|
SUPPORTS,
|
||||||
NOT_SUPPORTED,
|
NOT_SUPPORTED,
|
||||||
MANDATORY
|
MANDATORY
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-31
@@ -2,83 +2,78 @@ package dev.relism.flash.ext.data.core;
|
|||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class Tx {
|
public final class Tx {
|
||||||
private static final ThreadLocal<Deque<TxStatus>> STATUS_STACK =
|
private static final ThreadLocal<Deque<TxStatus>> STATUS_STACK =
|
||||||
ThreadLocal.withInitial(ArrayDeque::new);
|
ThreadLocal.withInitial(ArrayDeque::new);
|
||||||
private static volatile TxManager manager;
|
private final TxManager manager;
|
||||||
|
|
||||||
private Tx() {}
|
public Tx(TxManager txManager) {
|
||||||
|
this.manager = Objects.requireNonNull(txManager);
|
||||||
public static void init(TxManager txManager) {
|
|
||||||
if (manager != null) {
|
|
||||||
throw new IllegalStateException("TxManager already initialized");
|
|
||||||
}
|
|
||||||
manager = txManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void run(TxRunnable work) {
|
public void run(TxRunnable work) {
|
||||||
run(TxDefinition.DEFAULTS, work);
|
run(TxDefinition.DEFAULTS, work);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void run(TxDefinition definition, TxRunnable work) {
|
public void run(TxDefinition definition, TxRunnable work) {
|
||||||
call(definition, () -> {
|
call(definition, () -> {
|
||||||
work.run();
|
work.run();
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T call(TxCallable<T> work) {
|
public <T> T call(TxCallable<T> work) {
|
||||||
return call(TxDefinition.DEFAULTS, work);
|
return call(TxDefinition.DEFAULTS, work);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T call(TxDefinition definition, TxCallable<T> work) {
|
public <T> T call(TxDefinition definition, TxCallable<T> work) {
|
||||||
TxStatus status = manager().begin(definition);
|
TxStatus status = manager.begin(definition);
|
||||||
pushStatus(status);
|
pushStatus(status);
|
||||||
try {
|
try {
|
||||||
T result = work.call();
|
T result = work.call();
|
||||||
if (status.isRollbackOnly()) {
|
if (status.isRollbackOnly()) {
|
||||||
manager().rollback(status);
|
manager.rollback(status);
|
||||||
} else {
|
} else {
|
||||||
manager().commit(status);
|
manager.commit(status);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
manager().rollback(status);
|
silentRollback(status);
|
||||||
throw (e instanceof TxException txException) ? txException : new TxException(e);
|
throw (e instanceof TxException txException) ? txException : new TxException(e);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
silentRollback(status);
|
||||||
|
throw sneakyThrow(t);
|
||||||
} finally {
|
} finally {
|
||||||
popStatus();
|
popStatus();
|
||||||
|
if (STATUS_STACK.get().isEmpty()) {
|
||||||
|
STATUS_STACK.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isActive() {
|
public boolean isActive() {
|
||||||
return !STATUS_STACK.get().isEmpty();
|
return !STATUS_STACK.get().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setRollbackOnly() {
|
public void setRollbackOnly() {
|
||||||
currentStatus().markRollbackOnly();
|
currentStatus().markRollbackOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <R> R resource(Class<R> type) {
|
public <R> R resource(Class<R> type) {
|
||||||
return currentStatus().resource(type);
|
return currentStatus().resource(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TxDefinition requiresNew() {
|
public TxDefinition requiresNew() {
|
||||||
return TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW);
|
return TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TxDefinition readOnly() {
|
public TxDefinition readOnly() {
|
||||||
return TxDefinition.DEFAULTS.asReadOnly();
|
return TxDefinition.DEFAULTS.asReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TxManager manager() {
|
private TxStatus currentStatus() {
|
||||||
if (manager == null) {
|
|
||||||
throw new IllegalStateException("No TxManager installed");
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TxStatus currentStatus() {
|
|
||||||
TxStatus status = STATUS_STACK.get().peek();
|
TxStatus status = STATUS_STACK.get().peek();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
throw new IllegalStateException("No active transaction");
|
throw new IllegalStateException("No active transaction");
|
||||||
@@ -86,17 +81,29 @@ public final class Tx {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void pushStatus(TxStatus status) {
|
private void pushStatus(TxStatus status) {
|
||||||
STATUS_STACK.get().push(status);
|
STATUS_STACK.get().push(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void popStatus() {
|
private void popStatus() {
|
||||||
Deque<TxStatus> stack = STATUS_STACK.get();
|
Deque<TxStatus> stack = STATUS_STACK.get();
|
||||||
if (!stack.isEmpty()) {
|
if (!stack.isEmpty()) {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void silentRollback(TxStatus status) {
|
||||||
|
try {
|
||||||
|
manager.rollback(status);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <E extends Throwable> RuntimeException sneakyThrow(Throwable t) throws E {
|
||||||
|
throw (E) t;
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface TxRunnable {
|
public interface TxRunnable {
|
||||||
void run();
|
void run();
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# flash-ext-data-hibernate
|
||||||
|
|
||||||
|
Backend Hibernate per `flash-ext-data-core`.
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Questo modulo implementa `TxManager` sopra `SessionFactory` e fornisce una base repository Hibernate-centric.
|
||||||
|
|
||||||
|
## Come si usa
|
||||||
|
|
||||||
|
### 1. Creare il manager
|
||||||
|
|
||||||
|
```java
|
||||||
|
SessionFactory sessionFactory = ...;
|
||||||
|
HibernateTxManager txManager = new HibernateTxManager(sessionFactory);
|
||||||
|
DataExtension extension = new DataExtension(txManager);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Installare l’estensione in Flash
|
||||||
|
|
||||||
|
L’estensione registra `Tx` e `TxManager` nel `FlashContext`.
|
||||||
|
Le handler class-based annotate con `@Transactional` vengono wrappate automaticamente.
|
||||||
|
|
||||||
|
### 3. Definire una repository
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class UserRepository extends HibernateRepository<User, Long> {
|
||||||
|
public UserRepository(Tx tx) {
|
||||||
|
super(tx, User.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Con il nuovo modello query/spec puoi esporre campi riusabili come costanti:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class UserRepository extends HibernateRepository<User, Long> {
|
||||||
|
public static final SpecBuilder.FieldSpec<User, String> EMAIL = SpecBuilder.field("u.email");
|
||||||
|
public static final SpecBuilder.FieldSpec<User, Boolean> ACTIVE = SpecBuilder.field("u.active");
|
||||||
|
|
||||||
|
public UserRepository(Tx tx) {
|
||||||
|
super(tx, User.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<User> findByEmail(String email) {
|
||||||
|
return findOne(EMAIL.eq(email));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le query domain-specific possono usare gli helper della base class:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public List<User> findByEmailDomain(String domain) {
|
||||||
|
return findMany("from User u where u.email like :email", q ->
|
||||||
|
q.setParameter("email", "%@" + domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Come funziona sotto
|
||||||
|
|
||||||
|
- La tx corrente è rappresentata da `HibernateTxStatus`.
|
||||||
|
- La risorsa esposta al core è una `Session`.
|
||||||
|
- `Tx.resource(Session.class)` recupera la `Session` dal contesto corrente.
|
||||||
|
- `REQUIRES_NEW` sospende lo status attivo e apre una nuova `Session`.
|
||||||
|
- `NOT_SUPPORTED` sospende la tx attiva e continua senza sessione bindata.
|
||||||
|
|
||||||
|
## Repository base
|
||||||
|
|
||||||
|
`HibernateRepository` fornisce:
|
||||||
|
|
||||||
|
- `findById`, `findAll`, `findPage`, `findOne`
|
||||||
|
- `save`, `update`, `delete`, `saveAll`
|
||||||
|
- bulk `deleteAll(Spec<T>)` e `updateAll(Spec<T>, T)`
|
||||||
|
- helper HQL: `hql(...)`, `hqlMutate(...)`
|
||||||
|
|
||||||
|
Le classi concrete devono solo implementare query di dominio, non il plumbing transazionale.
|
||||||
|
|
||||||
|
## Semantica transazionale
|
||||||
|
|
||||||
|
- `REQUIRED`: join o apertura nuova tx.
|
||||||
|
- `REQUIRES_NEW`: sospensione del contesto corrente.
|
||||||
|
- `SUPPORTS`: join se c’è tx, altrimenti no-op.
|
||||||
|
- `NOT_SUPPORTED`: sospende e prosegue senza tx.
|
||||||
|
- `MANDATORY`: fallisce se non c’è tx.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- `Session` viene chiusa a fine tx nuova.
|
||||||
|
- Le synchronizations vengono eseguite al commit/rollback.
|
||||||
|
- Il backend è pensato per essere usato tramite la base class, non direttamente.
|
||||||
+77
-87
@@ -1,79 +1,74 @@
|
|||||||
package dev.relism.flash.ext.data.hibernate;
|
package dev.relism.flash.ext.data.hibernate;
|
||||||
|
|
||||||
import dev.relism.flash.ext.data.core.*;
|
import dev.relism.flash.ext.data.core.*;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.hibernate.query.MutationQuery;
|
import org.hibernate.query.MutationQuery;
|
||||||
|
|
||||||
import jakarta.persistence.TypedQuery;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
public abstract class HibernateRepository<T, ID extends Serializable> extends Repository<T, ID> {
|
||||||
* Hibernate-backed repository base.
|
|
||||||
* Never extend this directly — extend {@link Repository} from the core.
|
|
||||||
* This class is instantiated internally by flash-ext-data-hibernate.
|
|
||||||
*/
|
|
||||||
public abstract class HibernateRepository<T, ID extends Serializable>
|
|
||||||
extends Repository<T, ID> {
|
|
||||||
|
|
||||||
private final Class<T> type;
|
private final Class<T> type;
|
||||||
|
|
||||||
protected HibernateRepository(Class<T> type) {
|
protected HibernateRepository(Tx tx, Class<T> type) {
|
||||||
|
super(tx);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Session — always safe, tx() wrapper guarantees active transaction ─────
|
|
||||||
|
|
||||||
protected Session session() {
|
protected Session session() {
|
||||||
return Tx.resource(Session.class);
|
return tx().resource(Session.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Repository abstract impl ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<T> doFindById(ID id) {
|
protected Optional<T> doFindById(ID id) {
|
||||||
return Optional.ofNullable(session().get(type, id));
|
return Optional.ofNullable(session().get(type, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll() {
|
protected List<T> doFind(Query<T> query) {
|
||||||
return hql("from " + type.getSimpleName()).getResultList();
|
HibernateSpecContext ctx = new HibernateSpecContext();
|
||||||
|
String where = query.spec() != null ? " where " + query.spec().toFragment(ctx) : "";
|
||||||
|
String order = query.sort() != null && query.sort().isSorted() ? " order by " + orderClause(query.sort()) : "";
|
||||||
|
|
||||||
|
TypedQuery<T> q = session().createQuery("from " + type.getSimpleName() + where + order, type);
|
||||||
|
ctx.applyParameters(q);
|
||||||
|
|
||||||
|
if (query.isPaged()) {
|
||||||
|
q.setFirstResult(query.page() * query.size());
|
||||||
|
q.setMaxResults(query.size());
|
||||||
|
}
|
||||||
|
return q.getResultList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll(int page, int size) {
|
protected Optional<T> doFindOne(Spec<T> spec) {
|
||||||
return hql("from " + type.getSimpleName())
|
return doFind(Query.<T>all().where(spec).page(0, 1)).stream().findFirst();
|
||||||
.setFirstResult(page * size)
|
|
||||||
.setMaxResults(size)
|
|
||||||
.getResultList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll(Sort sort) {
|
protected Page<T> doFindPage(Query<T> query) {
|
||||||
return hql("from " + type.getSimpleName() + orderClause(sort))
|
if (!query.isPaged()) {
|
||||||
.getResultList();
|
throw new IllegalArgumentException("Paged query requires page and size");
|
||||||
|
}
|
||||||
|
long total = countWhere(query.spec());
|
||||||
|
List<T> content = doFind(query);
|
||||||
|
return new Page<>(content, query.page(), query.size(), total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll(int page, int size, Sort sort) {
|
protected boolean doExistsById(ID id) {
|
||||||
return hql("from " + type.getSimpleName() + orderClause(sort))
|
return doFindById(id).isPresent();
|
||||||
.setFirstResult(page * size)
|
|
||||||
.setMaxResults(size)
|
|
||||||
.getResultList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<T> doFindPage(int page, int size) {
|
protected long doCount() {
|
||||||
long total = doCount();
|
return countWhere(Spec.all());
|
||||||
return new Page<>(doFindAll(page, size), page, size, total);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Page<T> doFindPage(int page, int size, Sort sort) {
|
|
||||||
long total = doCount();
|
|
||||||
return new Page<>(doFindAll(page, size, sort), page, size, total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -82,6 +77,22 @@ public abstract class HibernateRepository<T, ID extends Serializable>
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<T> doSaveAll(Iterable<T> entities) {
|
||||||
|
List<T> saved = new ArrayList<>();
|
||||||
|
Session s = session();
|
||||||
|
int i = 0;
|
||||||
|
for (T entity : entities) {
|
||||||
|
s.persist(entity);
|
||||||
|
saved.add(entity);
|
||||||
|
if (++i % 50 == 0) {
|
||||||
|
s.flush();
|
||||||
|
s.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected T doUpdate(T entity) {
|
protected T doUpdate(T entity) {
|
||||||
return session().merge(entity);
|
return session().merge(entity);
|
||||||
@@ -99,66 +110,37 @@ public abstract class HibernateRepository<T, ID extends Serializable>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doExistsById(ID id) {
|
protected int doDeleteAll(Spec<T> spec) {
|
||||||
return doFindById(id).isPresent();
|
HibernateSpecContext ctx = new HibernateSpecContext();
|
||||||
|
String where = " where " + spec.toFragment(ctx);
|
||||||
|
MutationQuery q = session().createMutationQuery("delete from " + type.getSimpleName() + where);
|
||||||
|
ctx.applyParameters(q);
|
||||||
|
return q.executeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long doCount() {
|
protected int doUpdateAll(Spec<T> spec, T patch) {
|
||||||
return session()
|
throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
|
||||||
.createQuery("select count(*) from " + type.getSimpleName(), Long.class)
|
|
||||||
.uniqueResultOptional()
|
|
||||||
.orElse(0L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Query helpers — usabili nelle sottoclassi domain ─────────────────────
|
protected List<T> hql(String hql, Consumer<TypedQuery<T>> params) {
|
||||||
|
return roQuery(() -> {
|
||||||
protected TypedQuery<T> hql(String hql) {
|
TypedQuery<T> q = session().createQuery(hql, type);
|
||||||
return session().createQuery(hql, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected <R> TypedQuery<R> hql(String hql, Class<R> resultType) {
|
|
||||||
return session().createQuery(hql, resultType);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Optional<T> findOne(String hql, Consumer<TypedQuery<T>> params) {
|
|
||||||
TypedQuery<T> q = hql(hql);
|
|
||||||
params.accept(q);
|
|
||||||
return q.getResultStream().findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params) {
|
|
||||||
return tx(() -> {
|
|
||||||
TypedQuery<T> q = hql(hql);
|
|
||||||
params.accept(q);
|
params.accept(q);
|
||||||
return q.getResultList();
|
return q.getResultList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params,
|
protected <R> List<R> hql(String hql, Class<R> resultType, Consumer<TypedQuery<R>> params) {
|
||||||
int page, int size) {
|
return roQuery(() -> {
|
||||||
return tx(() -> {
|
TypedQuery<R> q = session().createQuery(hql, resultType);
|
||||||
TypedQuery<T> q = hql(hql);
|
|
||||||
params.accept(q);
|
params.accept(q);
|
||||||
return q.setFirstResult(page * size).setMaxResults(size).getResultList();
|
return q.getResultList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Page<T> findManyPaged(String hql, String countHql,
|
protected int hqlMutate(String hql, Consumer<MutationQuery> params) {
|
||||||
Consumer<TypedQuery<T>> params,
|
return rwQuery(() -> {
|
||||||
int page, int size) {
|
|
||||||
return tx(() -> {
|
|
||||||
long total = session()
|
|
||||||
.createQuery(countHql, Long.class)
|
|
||||||
.uniqueResultOptional()
|
|
||||||
.orElse(0L);
|
|
||||||
List<T> content = findMany(hql, params, page, size);
|
|
||||||
return new Page<>(content, page, size, total);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int execute(String hql, Consumer<MutationQuery> params) {
|
|
||||||
return tx(() -> {
|
|
||||||
MutationQuery q = session().createMutationQuery(hql);
|
MutationQuery q = session().createMutationQuery(hql);
|
||||||
params.accept(q);
|
params.accept(q);
|
||||||
return q.executeUpdate();
|
return q.executeUpdate();
|
||||||
@@ -169,9 +151,17 @@ public abstract class HibernateRepository<T, ID extends Serializable>
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long countWhere(Spec<T> spec) {
|
||||||
|
HibernateSpecContext ctx = new HibernateSpecContext();
|
||||||
|
String where = spec != null ? " where " + spec.toFragment(ctx) : "";
|
||||||
|
TypedQuery<Long> q = session().createQuery("select count(*) from " + type.getSimpleName() + where, Long.class);
|
||||||
|
ctx.applyParameters(q);
|
||||||
|
return q.getResultStream().findFirst().orElse(0L);
|
||||||
|
}
|
||||||
|
|
||||||
private String orderClause(Sort sort) {
|
private String orderClause(Sort sort) {
|
||||||
return " order by " + sort.columns().stream()
|
return sort.columns().stream()
|
||||||
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
|
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.relism.flash.ext.data.hibernate;
|
||||||
|
|
||||||
|
import dev.relism.flash.ext.data.core.SpecContext;
|
||||||
|
import jakarta.persistence.TypedQuery;
|
||||||
|
import org.hibernate.query.MutationQuery;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class HibernateSpecContext implements SpecContext {
|
||||||
|
private final Map<String, Object> params = new LinkedHashMap<>();
|
||||||
|
private int counter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bind(Object value) {
|
||||||
|
String name = "p" + (++counter);
|
||||||
|
params.put(name, value);
|
||||||
|
return ":" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyParameters(TypedQuery<?> query) {
|
||||||
|
params.forEach(query::setParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyParameters(MutationQuery query) {
|
||||||
|
params.forEach(query::setParameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
-19
@@ -8,6 +8,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public class HibernateTxManager implements TxManager {
|
public class HibernateTxManager implements TxManager {
|
||||||
private static final TxResourceKey HIBERNATE_STATUS_KEY = TxResourceKey.of("hibernate.tx.status");
|
private static final TxResourceKey HIBERNATE_STATUS_KEY = TxResourceKey.of("hibernate.tx.status");
|
||||||
|
private static final TxResourceKey HIBERNATE_SUSPENDED_KEY = TxResourceKey.of("hibernate.tx.suspended");
|
||||||
|
|
||||||
private final SessionFactory sf;
|
private final SessionFactory sf;
|
||||||
|
|
||||||
@@ -22,35 +23,56 @@ public class HibernateTxManager implements TxManager {
|
|||||||
? joinExisting(definition)
|
? joinExisting(definition)
|
||||||
: beginNew(definition);
|
: beginNew(definition);
|
||||||
case REQUIRES_NEW -> beginNew(definition);
|
case REQUIRES_NEW -> beginNew(definition);
|
||||||
|
case SUPPORTS -> ResourceRegistry.isBound(HIBERNATE_STATUS_KEY)
|
||||||
|
? joinExisting(definition)
|
||||||
|
: noOp(definition);
|
||||||
case MANDATORY -> {
|
case MANDATORY -> {
|
||||||
if (!ResourceRegistry.isBound(HIBERNATE_STATUS_KEY))
|
if (!ResourceRegistry.isBound(HIBERNATE_STATUS_KEY))
|
||||||
throw new IllegalStateException("MANDATORY: no active transaction");
|
throw new IllegalStateException("MANDATORY: no active transaction");
|
||||||
yield joinExisting(definition);
|
yield joinExisting(definition);
|
||||||
}
|
}
|
||||||
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
|
case NOT_SUPPORTED -> {
|
||||||
|
HibernateTxStatus suspended = suspendIfNeeded();
|
||||||
|
yield noOp(definition, suspended);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private TxStatus beginNew(TxDefinition definition) {
|
private TxStatus beginNew(TxDefinition definition) {
|
||||||
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
|
return beginNew(definition, suspendIfNeeded());
|
||||||
if (suspended != null) {
|
}
|
||||||
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
|
||||||
}
|
private TxStatus beginNew(TxDefinition definition, HibernateTxStatus suspended) {
|
||||||
Session s = sf.openSession();
|
Session s = sf.openSession();
|
||||||
s.beginTransaction();
|
boolean bound = false;
|
||||||
if (definition.readOnly()) s.setDefaultReadOnly(true);
|
try {
|
||||||
if (definition.isolation() != TransactionIsolation.DEFAULT) {
|
s.beginTransaction();
|
||||||
s.doWork(connection -> connection.setTransactionIsolation(definition.isolation().level()));
|
if (definition.readOnly()) s.setDefaultReadOnly(true);
|
||||||
|
if (definition.isolation() != TransactionIsolation.DEFAULT) {
|
||||||
|
s.doWork(connection -> connection.setTransactionIsolation(definition.isolation().level()));
|
||||||
|
}
|
||||||
|
HibernateTxStatus status = new HibernateTxStatus(
|
||||||
|
s,
|
||||||
|
true,
|
||||||
|
definition.readOnly(),
|
||||||
|
suspended,
|
||||||
|
new HibernateTxStatus.RollbackMarker()
|
||||||
|
);
|
||||||
|
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, status);
|
||||||
|
bound = true;
|
||||||
|
return status;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
silentClose(s);
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
silentClose(s);
|
||||||
|
throw new TxException(e);
|
||||||
|
} finally {
|
||||||
|
if (!bound && suspended != null) {
|
||||||
|
ResourceRegistry.unbind(HIBERNATE_SUSPENDED_KEY);
|
||||||
|
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HibernateTxStatus status = new HibernateTxStatus(
|
|
||||||
s,
|
|
||||||
true,
|
|
||||||
definition.readOnly(),
|
|
||||||
suspended,
|
|
||||||
new HibernateTxStatus.RollbackMarker()
|
|
||||||
);
|
|
||||||
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, status);
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TxStatus joinExisting(TxDefinition definition) {
|
private TxStatus joinExisting(TxDefinition definition) {
|
||||||
@@ -67,10 +89,29 @@ public class HibernateTxManager implements TxManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TxStatus noOp(TxDefinition definition) {
|
||||||
|
return noOp(definition, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TxStatus noOp(TxDefinition definition, HibernateTxStatus suspended) {
|
||||||
|
return new HibernateTxStatus(null, false, definition.readOnly(), suspended, new HibernateTxStatus.RollbackMarker());
|
||||||
|
}
|
||||||
|
|
||||||
|
private HibernateTxStatus suspendIfNeeded() {
|
||||||
|
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
|
||||||
|
if (suspended != null) {
|
||||||
|
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
||||||
|
ResourceRegistry.bind(HIBERNATE_SUSPENDED_KEY, suspended);
|
||||||
|
}
|
||||||
|
return suspended;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void commit(TxStatus status) {
|
public void commit(TxStatus status) {
|
||||||
HibernateTxStatus s = (HibernateTxStatus) status;
|
HibernateTxStatus s = (HibernateTxStatus) status;
|
||||||
if (!s.isNewTransaction()) {
|
if (!s.isNewTransaction()) {
|
||||||
|
resumeIfNeeded(s);
|
||||||
|
cleanupIfIdle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -83,6 +124,7 @@ public class HibernateTxManager implements TxManager {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cleanupAndResume(s);
|
cleanupAndResume(s);
|
||||||
|
cleanupIfIdle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +133,8 @@ public class HibernateTxManager implements TxManager {
|
|||||||
HibernateTxStatus s = (HibernateTxStatus) status;
|
HibernateTxStatus s = (HibernateTxStatus) status;
|
||||||
if (!s.isNewTransaction()) {
|
if (!s.isNewTransaction()) {
|
||||||
s.markRollbackOnly();
|
s.markRollbackOnly();
|
||||||
|
resumeIfNeeded(s);
|
||||||
|
cleanupIfIdle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -100,15 +144,40 @@ public class HibernateTxManager implements TxManager {
|
|||||||
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupAndResume(s);
|
cleanupAndResume(s);
|
||||||
|
cleanupIfIdle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanupAndResume(HibernateTxStatus status) {
|
private void cleanupAndResume(HibernateTxStatus status) {
|
||||||
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
|
||||||
status.session().close();
|
silentClose(status.session());
|
||||||
|
resumeIfNeeded(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupIfIdle() {
|
||||||
|
if (!ResourceRegistry.isBound(HIBERNATE_STATUS_KEY) && !ResourceRegistry.isBound(HIBERNATE_SUSPENDED_KEY)) {
|
||||||
|
ResourceRegistry.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resumeIfNeeded(HibernateTxStatus status) {
|
||||||
HibernateTxStatus suspended = status.suspended();
|
HibernateTxStatus suspended = status.suspended();
|
||||||
|
if (suspended == null) {
|
||||||
|
suspended = ResourceRegistry.getOrNull(HIBERNATE_SUSPENDED_KEY, HibernateTxStatus.class);
|
||||||
|
}
|
||||||
if (suspended != null) {
|
if (suspended != null) {
|
||||||
|
ResourceRegistry.unbind(HIBERNATE_SUSPENDED_KEY);
|
||||||
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
|
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void silentClose(Session session) {
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -35,6 +35,9 @@ class HibernateTxStatus implements TxStatus {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <R> R resource(Class<R> type) {
|
public <R> R resource(Class<R> type) {
|
||||||
|
if (session == null) {
|
||||||
|
throw new IllegalStateException("No session bound to this transaction status");
|
||||||
|
}
|
||||||
return type.cast(session);
|
return type.cast(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# flash-ext-data-jdbc
|
||||||
|
|
||||||
|
Backend JDBC per `flash-ext-data-core`.
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Questo modulo implementa `TxManager` sopra `DataSource` e fornisce una base repository SQL raw.
|
||||||
|
|
||||||
|
## Come si usa
|
||||||
|
|
||||||
|
### 1. Creare il manager
|
||||||
|
|
||||||
|
```java
|
||||||
|
DataSource dataSource = ...;
|
||||||
|
JdbcTxManager txManager = new JdbcTxManager(dataSource);
|
||||||
|
DataExtension extension = new DataExtension(txManager);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Installare l’estensione in Flash
|
||||||
|
|
||||||
|
Come per Hibernate, `DataExtension` registra `Tx` nel `FlashContext` e abilita `@Transactional` sugli handler class-based.
|
||||||
|
|
||||||
|
### 3. Definire una repository
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class UserRepository extends JdbcRepository<User, Long> {
|
||||||
|
public UserRepository(Tx tx) {
|
||||||
|
super(tx, "users", "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected User mapRow(ResultSet rs) throws SQLException {
|
||||||
|
return new User(rs.getLong("id"), rs.getString("name"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Anche qui puoi esporre `Spec` riusabili e comporre query dal service layer:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class UserRepository extends JdbcRepository<User, Long> {
|
||||||
|
public static final SpecBuilder.FieldSpec<User, String> EMAIL = SpecBuilder.field("email");
|
||||||
|
|
||||||
|
public UserRepository(Tx tx) {
|
||||||
|
super(tx, "users", "id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Per il salvataggio e l’update devi fornire il binding esplicito:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
protected String insertSql() {
|
||||||
|
return "insert into users(name) values(?)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void bindInsert(PreparedStatement ps, User entity) throws SQLException {
|
||||||
|
ps.setString(1, entity.name());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Come funziona sotto
|
||||||
|
|
||||||
|
- La tx corrente espone una `Connection`.
|
||||||
|
- `Tx.resource(Connection.class)` recupera la connessione bindata al thread.
|
||||||
|
- `REQUIRES_NEW` sospende la connessione attiva e ne apre una nuova.
|
||||||
|
- `NOT_SUPPORTED` sospende il contesto e prosegue senza tx.
|
||||||
|
|
||||||
|
## Repository base
|
||||||
|
|
||||||
|
`JdbcRepository` fornisce:
|
||||||
|
|
||||||
|
- query `select` con `queryOne`, `queryMany`
|
||||||
|
- mutation con `mutate`
|
||||||
|
- persistenza con `doSave`, `doUpdate`
|
||||||
|
- paging con `doFindPage`
|
||||||
|
- bulk `deleteAll(Spec<T>)`
|
||||||
|
- helper raw `queryOne(...)`, `queryMany(...)`, `mutate(...)`
|
||||||
|
|
||||||
|
Le repository concrete devono solo tradurre tra `ResultSet` e dominio.
|
||||||
|
|
||||||
|
## Semantica transazionale
|
||||||
|
|
||||||
|
- `REQUIRED`: join o apertura nuova tx.
|
||||||
|
- `REQUIRES_NEW`: sospensione del contesto corrente.
|
||||||
|
- `SUPPORTS`: join se c’è tx, altrimenti no-op.
|
||||||
|
- `NOT_SUPPORTED`: sospende e prosegue senza tx.
|
||||||
|
- `MANDATORY`: fallisce se non c’è tx.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- La `Connection` viene chiusa a fine tx nuova.
|
||||||
|
- Le synchronizations vengono eseguite al commit/rollback.
|
||||||
|
- Se una repository usa `doDelete(T)`, il comportamento predefinito è non supportato: usare `deleteById` o override specifico.
|
||||||
+95
-63
@@ -3,87 +3,99 @@ package dev.relism.flash.ext.data.jdbc;
|
|||||||
import dev.relism.flash.ext.data.core.*;
|
import dev.relism.flash.ext.data.core.*;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
import java.util.stream.Collectors;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
||||||
|
|
||||||
private final String table;
|
private final String table;
|
||||||
private final String idColumn;
|
private final String idColumn;
|
||||||
|
|
||||||
protected JdbcRepository(String table, String idColumn) {
|
protected JdbcRepository(Tx tx, String table, String idColumn) {
|
||||||
this.table = table;
|
super(tx);
|
||||||
|
this.table = table;
|
||||||
this.idColumn = idColumn;
|
this.idColumn = idColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Connection connection() {
|
protected Connection connection() {
|
||||||
return Tx.resource(Connection.class);
|
return tx().resource(Connection.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Subclass contract ─────────────────────────────────────────────────────
|
protected abstract T mapRow(ResultSet rs) throws SQLException;
|
||||||
|
protected abstract void bindInsert(PreparedStatement ps, T entity) throws SQLException;
|
||||||
protected abstract T mapRow(ResultSet rs) throws SQLException;
|
protected abstract void bindUpdate(PreparedStatement ps, T entity) throws SQLException;
|
||||||
protected abstract void bindInsert(PreparedStatement ps, T entity) throws SQLException;
|
|
||||||
protected abstract void bindUpdate(PreparedStatement ps, T entity) throws SQLException;
|
|
||||||
protected abstract String insertSql();
|
protected abstract String insertSql();
|
||||||
protected abstract String updateSql();
|
protected abstract String updateSql();
|
||||||
|
|
||||||
// ── Repository abstract impl ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<T> doFindById(ID id) {
|
protected Optional<T> doFindById(ID id) {
|
||||||
return queryOne("select * from " + table + " where " + idColumn + " = ?",
|
return queryOne("select * from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id));
|
||||||
ps -> ps.setObject(1, id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll() {
|
protected List<T> doFind(Query<T> query) {
|
||||||
return queryMany("select * from " + table, ps -> {});
|
JdbcSpecContext ctx = new JdbcSpecContext();
|
||||||
}
|
String where = query.spec() != null ? " where " + query.spec().toFragment(ctx) : "";
|
||||||
|
String order = query.sort() != null && query.sort().isSorted() ? " order by " + orderClause(query.sort()) : "";
|
||||||
|
String paging = query.isPaged() ? " limit ? offset ?" : "";
|
||||||
|
|
||||||
@Override
|
return queryMany("select * from " + table + where + order + paging, ps -> {
|
||||||
protected List<T> doFindAll(int page, int size) {
|
if (query.isPaged()) {
|
||||||
return queryMany("select * from " + table + " limit ? offset ?", ps -> {
|
ctx.applyParameters(ps);
|
||||||
ps.setInt(1, size);
|
int base = ctx.size();
|
||||||
ps.setInt(2, page * size);
|
ps.setInt(base + 1, query.size());
|
||||||
|
ps.setInt(base + 2, query.page() * query.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.applyParameters(ps);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll(Sort sort) {
|
protected Optional<T> doFindOne(Spec<T> spec) {
|
||||||
return queryMany("select * from " + table + orderClause(sort), ps -> {});
|
return doFind(Query.<T>all().where(spec).page(0, 1)).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<T> doFindAll(int page, int size, Sort sort) {
|
protected Page<T> doFindPage(Query<T> query) {
|
||||||
return queryMany("select * from " + table + orderClause(sort) + " limit ? offset ?",
|
if (!query.isPaged()) {
|
||||||
ps -> {
|
throw new IllegalArgumentException("Paged query requires page and size");
|
||||||
ps.setInt(1, size);
|
}
|
||||||
ps.setInt(2, page * size);
|
long total = countWhere(query.spec());
|
||||||
});
|
List<T> content = doFind(query);
|
||||||
|
return new Page<>(content, query.page(), query.size(), total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<T> doFindPage(int page, int size) {
|
protected boolean doExistsById(ID id) {
|
||||||
long total = doCount();
|
return queryOne("select 1 from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id), rs -> rs.getInt(1)).isPresent();
|
||||||
return new Page<>(doFindAll(page, size), page, size, total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<T> doFindPage(int page, int size, Sort sort) {
|
protected long doCount() {
|
||||||
long total = doCount();
|
return queryOne("select count(*) from " + table, ps -> {}, rs -> rs.getLong(1)).orElse(0L);
|
||||||
return new Page<>(doFindAll(page, size, sort), page, size, total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected T doSave(T entity) {
|
protected T doSave(T entity) {
|
||||||
try (PreparedStatement ps = connection().prepareStatement(
|
try (PreparedStatement ps = connection().prepareStatement(insertSql(), Statement.RETURN_GENERATED_KEYS)) {
|
||||||
insertSql(), Statement.RETURN_GENERATED_KEYS)) {
|
|
||||||
bindInsert(ps, entity);
|
bindInsert(ps, entity);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
applyGeneratedKey(ps, entity);
|
applyGeneratedKey(ps, entity);
|
||||||
return entity;
|
return entity;
|
||||||
} catch (SQLException e) { throw new TxException(e); }
|
} catch (SQLException e) {
|
||||||
|
throw new TxException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<T> doSaveAll(Iterable<T> entities) {
|
||||||
|
List<T> saved = new ArrayList<>();
|
||||||
|
for (T entity : entities) {
|
||||||
|
saved.add(doSave(entity));
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,7 +104,9 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
|||||||
bindUpdate(ps, entity);
|
bindUpdate(ps, entity);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
return entity;
|
return entity;
|
||||||
} catch (SQLException e) { throw new TxException(e); }
|
} catch (SQLException e) {
|
||||||
|
throw new TxException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -102,38 +116,35 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doDeleteById(ID id) {
|
protected void doDeleteById(ID id) {
|
||||||
mutate("delete from " + table + " where " + idColumn + " = ?",
|
mutate("delete from " + table + " where " + idColumn + " = ?", ps -> ps.setObject(1, id));
|
||||||
ps -> ps.setObject(1, id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doExistsById(ID id) {
|
protected int doDeleteAll(Spec<T> spec) {
|
||||||
return queryOne("select 1 from " + table + " where " + idColumn + " = ?",
|
JdbcSpecContext ctx = new JdbcSpecContext();
|
||||||
ps -> ps.setObject(1, id),
|
String where = " where " + spec.toFragment(ctx);
|
||||||
rs -> rs.getInt(1)).isPresent();
|
return mutate("delete from " + table + where, ctx::applyParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long doCount() {
|
protected int doUpdateAll(Spec<T> spec, T patch) {
|
||||||
return queryOne("select count(*) from " + table, ps -> {},
|
throw new UnsupportedOperationException("Override doUpdateAll() for bulk UPDATE support");
|
||||||
rs -> rs.getLong(1)).orElse(0L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Query helpers ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
protected Optional<T> queryOne(String sql, SqlBinder params) {
|
protected Optional<T> queryOne(String sql, SqlBinder params) {
|
||||||
List<T> r = queryMany(sql, params);
|
List<T> r = queryMany(sql, params);
|
||||||
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
|
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected <R> Optional<R> queryOne(String sql, SqlBinder params,
|
protected <R> Optional<R> queryOne(String sql, SqlBinder params, SqlMapper<R> mapper) {
|
||||||
SqlMapper<R> mapper) {
|
|
||||||
try (PreparedStatement ps = connection().prepareStatement(sql)) {
|
try (PreparedStatement ps = connection().prepareStatement(sql)) {
|
||||||
params.bind(ps);
|
params.bind(ps);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
return rs.next() ? Optional.of(mapper.map(rs)) : Optional.empty();
|
return rs.next() ? Optional.of(mapper.map(rs)) : Optional.empty();
|
||||||
}
|
}
|
||||||
} catch (SQLException e) { throw new TxException(e); }
|
} catch (SQLException e) {
|
||||||
|
throw new TxException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<T> queryMany(String sql, SqlBinder params) {
|
protected List<T> queryMany(String sql, SqlBinder params) {
|
||||||
@@ -144,26 +155,47 @@ public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
|
|||||||
while (rs.next()) results.add(mapRow(rs));
|
while (rs.next()) results.add(mapRow(rs));
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
} catch (SQLException e) { throw new TxException(e); }
|
} catch (SQLException e) {
|
||||||
|
throw new TxException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int mutate(String sql, SqlBinder params) {
|
protected int mutate(String sql, SqlBinder params) {
|
||||||
try (PreparedStatement ps = connection().prepareStatement(sql)) {
|
try (PreparedStatement ps = connection().prepareStatement(sql)) {
|
||||||
params.bind(ps);
|
params.bind(ps);
|
||||||
return ps.executeUpdate();
|
return ps.executeUpdate();
|
||||||
} catch (SQLException e) { throw new TxException(e); }
|
} catch (SQLException e) {
|
||||||
|
throw new TxException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void applyGeneratedKey(PreparedStatement ps, T entity) throws SQLException {
|
protected void applyGeneratedKey(PreparedStatement ps, T entity) throws SQLException {
|
||||||
// override when entity has a generated PK
|
// override when entity has a generated PK
|
||||||
}
|
}
|
||||||
|
|
||||||
private String orderClause(Sort sort) {
|
protected Class<T> entityType() {
|
||||||
return " order by " + sort.columns().stream()
|
return null;
|
||||||
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
|
|
||||||
.collect(Collectors.joining(", "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface public interface SqlBinder { void bind(PreparedStatement ps) throws SQLException; }
|
private long countWhere(Spec<T> spec) {
|
||||||
@FunctionalInterface public interface SqlMapper<R> { R map(ResultSet rs) throws SQLException; }
|
JdbcSpecContext ctx = new JdbcSpecContext();
|
||||||
}
|
String where = spec != null ? " where " + spec.toFragment(ctx) : "";
|
||||||
|
return queryOne("select count(*) from " + table + where, ctx::applyParameters, rs -> rs.getLong(1)).orElse(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String orderClause(Sort sort) {
|
||||||
|
return sort.columns().stream()
|
||||||
|
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
|
||||||
|
.collect(java.util.stream.Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SqlBinder {
|
||||||
|
void bind(PreparedStatement ps) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SqlMapper<R> {
|
||||||
|
R map(ResultSet rs) throws SQLException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.relism.flash.ext.data.jdbc;
|
||||||
|
|
||||||
|
import dev.relism.flash.ext.data.core.SpecContext;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class JdbcSpecContext implements SpecContext {
|
||||||
|
private final List<Object> params = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bind(Object value) {
|
||||||
|
params.add(value);
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyParameters(PreparedStatement ps) throws SQLException {
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
ps.setObject(i + 1, params.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() {
|
||||||
|
return params.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-7
@@ -9,6 +9,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public class JdbcTxManager implements TxManager {
|
public class JdbcTxManager implements TxManager {
|
||||||
private static final TxResourceKey JDBC_STATUS_KEY = TxResourceKey.of("jdbc.tx.status");
|
private static final TxResourceKey JDBC_STATUS_KEY = TxResourceKey.of("jdbc.tx.status");
|
||||||
|
private static final TxResourceKey JDBC_SUSPENDED_KEY = TxResourceKey.of("jdbc.tx.suspended");
|
||||||
|
|
||||||
private final DataSource ds;
|
private final DataSource ds;
|
||||||
|
|
||||||
@@ -23,22 +24,27 @@ public class JdbcTxManager implements TxManager {
|
|||||||
? joinExisting(definition)
|
? joinExisting(definition)
|
||||||
: beginNew(definition);
|
: beginNew(definition);
|
||||||
case REQUIRES_NEW -> beginNew(definition);
|
case REQUIRES_NEW -> beginNew(definition);
|
||||||
|
case SUPPORTS -> ResourceRegistry.isBound(JDBC_STATUS_KEY)
|
||||||
|
? joinExisting(definition)
|
||||||
|
: noOp(definition);
|
||||||
case MANDATORY -> {
|
case MANDATORY -> {
|
||||||
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY))
|
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY))
|
||||||
throw new IllegalStateException("MANDATORY: no active transaction");
|
throw new IllegalStateException("MANDATORY: no active transaction");
|
||||||
yield joinExisting(definition);
|
yield joinExisting(definition);
|
||||||
}
|
}
|
||||||
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
|
case NOT_SUPPORTED -> {
|
||||||
|
JdbcTxStatus suspended = suspendIfNeeded();
|
||||||
|
yield noOp(definition, suspended);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private TxStatus beginNew(TxDefinition definition) {
|
private TxStatus beginNew(TxDefinition definition) {
|
||||||
|
Connection conn = null;
|
||||||
|
JdbcTxStatus suspended = suspendIfNeeded();
|
||||||
|
boolean bound = false;
|
||||||
try {
|
try {
|
||||||
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
|
conn = ds.getConnection();
|
||||||
if (suspended != null) {
|
|
||||||
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
|
||||||
}
|
|
||||||
Connection conn = ds.getConnection();
|
|
||||||
conn.setAutoCommit(false);
|
conn.setAutoCommit(false);
|
||||||
if (definition.readOnly()) conn.setReadOnly(true);
|
if (definition.readOnly()) conn.setReadOnly(true);
|
||||||
if (definition.isolation() != TransactionIsolation.DEFAULT) {
|
if (definition.isolation() != TransactionIsolation.DEFAULT) {
|
||||||
@@ -52,9 +58,16 @@ public class JdbcTxManager implements TxManager {
|
|||||||
new JdbcTxStatus.RollbackMarker()
|
new JdbcTxStatus.RollbackMarker()
|
||||||
);
|
);
|
||||||
ResourceRegistry.bind(JDBC_STATUS_KEY, status);
|
ResourceRegistry.bind(JDBC_STATUS_KEY, status);
|
||||||
|
bound = true;
|
||||||
return status;
|
return status;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
silentClose(conn);
|
||||||
throw new TxException(e);
|
throw new TxException(e);
|
||||||
|
} finally {
|
||||||
|
if (!bound && suspended != null) {
|
||||||
|
ResourceRegistry.unbind(JDBC_SUSPENDED_KEY);
|
||||||
|
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +85,29 @@ public class JdbcTxManager implements TxManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TxStatus noOp(TxDefinition definition) {
|
||||||
|
return noOp(definition, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TxStatus noOp(TxDefinition definition, JdbcTxStatus suspended) {
|
||||||
|
return new JdbcTxStatus(null, false, definition.readOnly(), suspended, new JdbcTxStatus.RollbackMarker());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JdbcTxStatus suspendIfNeeded() {
|
||||||
|
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
|
||||||
|
if (suspended != null) {
|
||||||
|
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
||||||
|
ResourceRegistry.bind(JDBC_SUSPENDED_KEY, suspended);
|
||||||
|
}
|
||||||
|
return suspended;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void commit(TxStatus status) {
|
public void commit(TxStatus status) {
|
||||||
JdbcTxStatus s = (JdbcTxStatus) status;
|
JdbcTxStatus s = (JdbcTxStatus) status;
|
||||||
if (!s.isNewTransaction()) {
|
if (!s.isNewTransaction()) {
|
||||||
|
resumeIfNeeded(s);
|
||||||
|
cleanupIfIdle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -90,6 +122,7 @@ public class JdbcTxManager implements TxManager {
|
|||||||
throw new TxException(e);
|
throw new TxException(e);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupAndResume(s);
|
cleanupAndResume(s);
|
||||||
|
cleanupIfIdle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +131,8 @@ public class JdbcTxManager implements TxManager {
|
|||||||
JdbcTxStatus s = (JdbcTxStatus) status;
|
JdbcTxStatus s = (JdbcTxStatus) status;
|
||||||
if (!s.isNewTransaction()) {
|
if (!s.isNewTransaction()) {
|
||||||
s.markRollbackOnly();
|
s.markRollbackOnly();
|
||||||
|
resumeIfNeeded(s);
|
||||||
|
cleanupIfIdle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -107,18 +142,45 @@ public class JdbcTxManager implements TxManager {
|
|||||||
throw new TxException(e);
|
throw new TxException(e);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupAndResume(s);
|
cleanupAndResume(s);
|
||||||
|
cleanupIfIdle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanupAndResume(JdbcTxStatus status) {
|
private void cleanupAndResume(JdbcTxStatus status) {
|
||||||
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
ResourceRegistry.unbind(JDBC_STATUS_KEY);
|
||||||
try {
|
try {
|
||||||
status.connection().close();
|
if (status.connection() != null) {
|
||||||
|
status.connection().close();
|
||||||
|
}
|
||||||
} catch (SQLException ignored) {
|
} catch (SQLException ignored) {
|
||||||
}
|
}
|
||||||
|
resumeIfNeeded(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupIfIdle() {
|
||||||
|
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY) && !ResourceRegistry.isBound(JDBC_SUSPENDED_KEY)) {
|
||||||
|
ResourceRegistry.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resumeIfNeeded(JdbcTxStatus status) {
|
||||||
JdbcTxStatus suspended = status.suspended();
|
JdbcTxStatus suspended = status.suspended();
|
||||||
|
if (suspended == null) {
|
||||||
|
suspended = ResourceRegistry.getOrNull(JDBC_SUSPENDED_KEY, JdbcTxStatus.class);
|
||||||
|
}
|
||||||
if (suspended != null) {
|
if (suspended != null) {
|
||||||
|
ResourceRegistry.unbind(JDBC_SUSPENDED_KEY);
|
||||||
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
|
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void silentClose(Connection connection) {
|
||||||
|
if (connection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -37,6 +37,9 @@ class JdbcTxStatus implements TxStatus {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <R> R resource(Class<R> type) {
|
public <R> R resource(Class<R> type) {
|
||||||
|
if (connection == null) {
|
||||||
|
throw new IllegalStateException("No connection bound to this transaction status");
|
||||||
|
}
|
||||||
return type.cast(connection);
|
return type.cast(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@ public class OpenApiExtension implements FlashExtension {
|
|||||||
"});\n" +
|
"});\n" +
|
||||||
"</script>\n" +
|
"</script>\n" +
|
||||||
"</body>\n" +
|
"</body>\n" +
|
||||||
"</html>";
|
"</html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOperationFromEvent(OpenApiBuilder builder, RouteEvent event) {
|
private static void addOperationFromEvent(OpenApiBuilder builder, RouteEvent event) {
|
||||||
|
|||||||
+6
@@ -12,6 +12,7 @@ import dev.relism.flash.models.RequestHandler;
|
|||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
import dev.relism.flash.routing.GET;
|
import dev.relism.flash.routing.GET;
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -165,6 +166,11 @@ class OpenApiExtensionTest {
|
|||||||
routes.put(method.name() + " " + path, handler);
|
routes.put(method.name() + " " + path, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addMiddleware(Middleware mw) {
|
protected void addMiddleware(Middleware mw) {
|
||||||
middlewares.add(mw);
|
middlewares.add(mw);
|
||||||
|
|||||||
+6
@@ -5,6 +5,7 @@ import dev.relism.flash.models.Request;
|
|||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
import dev.relism.flash.routing.GET;
|
import dev.relism.flash.routing.GET;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -172,6 +173,11 @@ class JteExtensionTest {
|
|||||||
routes.put(method.name() + " " + path, handler);
|
routes.put(method.name() + " " + path, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addMiddleware(dev.relism.flash.routing.Middleware mw) {
|
protected void addMiddleware(dev.relism.flash.routing.Middleware mw) {
|
||||||
mws.add(mw);
|
mws.add(mw);
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package dev.relism.flash;
|
package dev.relism.flash;
|
||||||
|
|
||||||
import dev.relism.flash.extension.FlashApp;
|
import dev.relism.flash.extension.FlashApp;
|
||||||
|
import dev.relism.flash.extension.FlashConfiguration;
|
||||||
|
import dev.relism.flash.http.ContentType;
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
import dev.relism.flash.http.HttpStatus;
|
||||||
import dev.relism.flash.models.Request;
|
import dev.relism.flash.models.Request;
|
||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
import dev.relism.fpr.core.ByteView;
|
|
||||||
import dev.relism.flash.http.ContentType;
|
|
||||||
import dev.relism.flash.http.HttpStatus;
|
|
||||||
import dev.relism.flash.extension.FlashConfiguration;
|
|
||||||
import dev.relism.flash.routing.AbstractRouter;
|
import dev.relism.flash.routing.AbstractRouter;
|
||||||
|
import dev.relism.flash.routing.AbstractWsRouter;
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -17,73 +22,163 @@ import java.net.InetSocketAddress;
|
|||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pure I/O transport layer. Owns the {@link ServerSocket}, the virtual-thread
|
* Pure I/O transport layer. Owns the {@link ServerSocket}, the virtual-thread
|
||||||
* executor, and the keep-alive accept loop. Routing is delegated to a single
|
* executor, and the keep-alive accept loop. Routing is delegated to HTTP and WS routers.
|
||||||
* {@link AbstractRouter}.
|
|
||||||
*
|
*
|
||||||
* <p>Package-private : use {@link FlashApp} as the single
|
* <h3>Allocation model (unchanged)</h3>
|
||||||
* entry point.
|
* <ul>
|
||||||
|
* <li>{@code LONG_BUF} (20 bytes) is the only {@link ThreadLocal} kept here.</li>
|
||||||
|
* <li>WS handshake SHA-1: {@link ThreadLocal}<{@link MessageDigest}> — one per
|
||||||
|
* accept thread (there are now {@code ACCEPT_THREADS} of them, not one).</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class HttpServer implements ServerHandle {
|
class HttpServer implements ServerHandle {
|
||||||
|
|
||||||
private final FlashConfiguration configuration;
|
// ── Tuning constants ──────────────────────────────────────────────────────
|
||||||
private final ServerSocket serverSocket;
|
|
||||||
private final AbstractRouter router;
|
|
||||||
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
|
||||||
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
|
|
||||||
private volatile boolean stopped = false;
|
|
||||||
private final AtomicReference<Thread> acceptThread = new AtomicReference<>();
|
|
||||||
|
|
||||||
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
/**
|
||||||
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
* Number of platform threads competing on {@code serverSocket.accept()}.
|
||||||
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
* Rule of thumb: number of available CPU cores, capped at 8.
|
||||||
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
* More than this rarely helps — accept is cheap; the bottleneck is usually
|
||||||
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
* the virtual-thread executor dispatching the connection handler.
|
||||||
private static final byte[] CONNECTION_KEEPALIVE = "Connection: keep-alive\r\n".getBytes(StandardCharsets.UTF_8);
|
*/
|
||||||
private static final byte[] TRANSFER_CHUNKED = "Transfer-Encoding: chunked\r\n".getBytes(StandardCharsets.UTF_8);
|
private static final int ACCEPT_THREADS = Math.min(Runtime.getRuntime().availableProcessors(), 8);
|
||||||
private static final byte[] FINAL_CHUNK = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
/**
|
||||||
|
* TCP listen backlog. The kernel holds up to this many fully-established
|
||||||
|
* (SYN+ACK sent, ACK received) connections waiting for accept().
|
||||||
|
* 4096 is safe on Linux; /proc/sys/net/core/somaxconn must be >= this value,
|
||||||
|
* or the kernel silently caps it. Raise somaxconn if needed:
|
||||||
|
* sysctl -w net.core.somaxconn=4096
|
||||||
|
*/
|
||||||
|
private static final int ACCEPT_BACKLOG = 4096;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket send/receive buffer sizes. Matched to the WS frame read buffer
|
||||||
|
* ({@link FlashConfiguration#getWsFrameBufferSize()}) so the kernel never
|
||||||
|
* needs to fragment a full frame into multiple TCP segments on the receive
|
||||||
|
* side, and never blocks a write waiting for the send buffer to drain.
|
||||||
|
*
|
||||||
|
* Linux default is 87380 bytes (rmem) / 16384 bytes (wmem). We raise both
|
||||||
|
* to 256 KB — a good fit for up to ~250 KB WS frames with no partial reads.
|
||||||
|
*/
|
||||||
|
private static final int SOCKET_BUF_SIZE = 256 * 1024;
|
||||||
|
|
||||||
|
// ── Instance fields ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final FlashConfiguration configuration;
|
||||||
|
private final ServerSocket serverSocket;
|
||||||
|
private final AbstractRouter router;
|
||||||
|
private final AbstractWsRouter wsRouter;
|
||||||
|
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
|
||||||
|
private volatile boolean stopped = false;
|
||||||
|
|
||||||
|
/** Latch that reaches 0 when all accept threads have exited. */
|
||||||
|
private final CountDownLatch acceptLatch = new CountDownLatch(ACCEPT_THREADS);
|
||||||
|
|
||||||
|
// ── Static byte constants (written once, read-only on hot path) ──────────
|
||||||
|
|
||||||
|
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONNECTION_KEEPALIVE = "Connection: keep-alive\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] TRANSFER_CHUNKED = "Transfer-Encoding: chunked\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] FINAL_CHUNK = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
|
private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// Per-thread scratch buffers — allocated once per VT, reused for every request.
|
private static final byte[] WS_HANDSHAKE_PREFIX =
|
||||||
private static final ThreadLocal<byte[]> LONG_BUF = ThreadLocal.withInitial(() -> new byte[20]);
|
("HTTP/1.1 101 Switching Protocols\r\n" +
|
||||||
private static final ThreadLocal<byte[]> CHUNK_BUF = ThreadLocal.withInitial(() -> new byte[8192]);
|
"Upgrade: websocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"Sec-WebSocket-Accept: ")
|
||||||
|
.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
private static final byte[] WS_HANDSHAKE_SUFFIX =
|
||||||
|
"\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
private static final byte[] WS_REJECT_400 =
|
||||||
|
"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||||
|
.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
|
||||||
HttpServer(FlashConfiguration configuration, AbstractRouter router) throws IOException {
|
private static final byte[] WS_GUID_BYTES =
|
||||||
|
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
|
||||||
|
private static final ThreadLocal<MessageDigest> SHA1 =
|
||||||
|
ThreadLocal.withInitial(() -> {
|
||||||
|
try { return MessageDigest.getInstance("SHA-1"); }
|
||||||
|
catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
private static final ThreadLocal<byte[]> LONG_BUF = ThreadLocal.withInitial(() -> new byte[20]);
|
||||||
|
|
||||||
|
private static final int SHA1_LEN = 20;
|
||||||
|
private static final int WS_ACCEPT_LEN = 28;
|
||||||
|
|
||||||
|
// ── Constructor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
HttpServer(FlashConfiguration configuration, AbstractRouter router, AbstractWsRouter wsRouter) throws IOException {
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.serverSocket = new ServerSocket(configuration.getPort());
|
|
||||||
this.router = router;
|
this.router = router;
|
||||||
|
this.wsRouter = wsRouter;
|
||||||
|
|
||||||
|
// Explicit bind with raised backlog.
|
||||||
|
// setReuseAddress(true) must be called BEFORE bind().
|
||||||
|
this.serverSocket = new ServerSocket();
|
||||||
|
this.serverSocket.setReuseAddress(true);
|
||||||
|
this.serverSocket.setReceiveBufferSize(SOCKET_BUF_SIZE);
|
||||||
|
this.serverSocket.bind(new InetSocketAddress(configuration.getPort()), ACCEPT_BACKLOG);
|
||||||
|
|
||||||
|
log.info("HttpServer bound on port {} (backlog={}, acceptThreads={})",
|
||||||
|
configuration.getPort(), ACCEPT_BACKLOG, ACCEPT_THREADS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
acceptThread.set(Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run));
|
for (int i = 0; i < ACCEPT_THREADS; i++) {
|
||||||
|
final int idx = i;
|
||||||
|
Thread.ofPlatform()
|
||||||
|
.name("flash-accept-" + idx)
|
||||||
|
.daemon(false)
|
||||||
|
.start(this::acceptLoop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startAndBlock() {
|
public void startAndBlock() {
|
||||||
start();
|
start();
|
||||||
try { acceptThread.get().join(); }
|
try { acceptLatch.await(); }
|
||||||
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void run() {
|
/**
|
||||||
while (!stopped) {
|
* Single accept loop body — runs on each of the {@code ACCEPT_THREADS}
|
||||||
try {
|
* platform threads. All threads block on the same {@link ServerSocket};
|
||||||
process(serverSocket.accept());
|
* the JVM ensures only one wakes per incoming connection (no thundering herd).
|
||||||
} catch (IOException e) {
|
*/
|
||||||
if (!stopped) log.error("Accept loop error", e);
|
private void acceptLoop() {
|
||||||
|
try {
|
||||||
|
while (!stopped) {
|
||||||
|
try {
|
||||||
|
process(serverSocket.accept());
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!stopped) log.error("Accept error", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
acceptLatch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,24 +199,62 @@ class HttpServer implements ServerHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hot-path ─────────────────────────────────────────────────────────────
|
// ── Hot-path ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void process(Socket socket) {
|
private void process(Socket socket) {
|
||||||
try {
|
try {
|
||||||
executorService.submit(() -> {
|
executorService.submit(() -> {
|
||||||
activeSockets.add(socket);
|
activeSockets.add(socket);
|
||||||
try (socket;
|
try (socket;
|
||||||
InputStream in = socket.getInputStream();
|
InputStream in = socket.getInputStream();
|
||||||
OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
|
OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
|
||||||
|
|
||||||
|
// TCP_NODELAY: disable Nagle's algorithm.
|
||||||
|
// Small WS frames (< MSS) are sent immediately rather than
|
||||||
|
// waiting up to 200 ms for more data to coalesce. Latency
|
||||||
|
// drops significantly at the cost of slightly more TCP segments
|
||||||
|
// under sustained bulk transfer — acceptable for interactive WS.
|
||||||
|
socket.setTcpNoDelay(true);
|
||||||
|
socket.setSendBufferSize(SOCKET_BUF_SIZE);
|
||||||
|
|
||||||
|
// rawOut is the unbuffered socket stream — passed to WebSocketSession
|
||||||
|
// directly. WS writes are already bulk (header + payload in two calls);
|
||||||
|
// with TCP_NODELAY the kernel ships them without Nagle delay, so no
|
||||||
|
// userspace buffer is needed and no flush() is required per frame.
|
||||||
|
// HTTP responses continue to use the BufferedOutputStream (out) because
|
||||||
|
// writeResponse() does many small individual writes that benefit from
|
||||||
|
// userspace coalescing before a single syscall.
|
||||||
|
OutputStream rawOut = socket.getOutputStream();
|
||||||
|
|
||||||
RequestParser parser = new RequestParser(
|
RequestParser parser = new RequestParser(
|
||||||
configuration.getMaxHeaderBufferSize(),
|
configuration.getMaxHeaderBufferSize(),
|
||||||
(InetSocketAddress) socket.getRemoteSocketAddress());
|
(InetSocketAddress) socket.getRemoteSocketAddress());
|
||||||
|
|
||||||
while (!stopped) {
|
while (!stopped) {
|
||||||
Request request = parser.parse(in);
|
Request request = parser.parse(in);
|
||||||
if (request == null) break;
|
if (request == null) break;
|
||||||
|
|
||||||
boolean keepAlive = isKeepAlive(request);
|
if (request.method() == HttpMethod.GET && isWebSocketUpgrade(request)) {
|
||||||
Response response = new Response(200, ContentType.TEXT_PLAIN);
|
WebSocketHandler wsHandler = wsRouter.route(request);
|
||||||
|
if (wsHandler == null) {
|
||||||
|
out.write(WS_REJECT_400);
|
||||||
|
out.flush();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Flush buffered HTTP bytes (the 101 response) before WebSocketSession
|
||||||
|
// takes over rawOut — otherwise the handshake reply stays stuck in
|
||||||
|
// the BufferedOutputStream buffer and the client never sees it.
|
||||||
|
performHandshake(out, request);
|
||||||
|
out.flush();
|
||||||
|
request.drain();
|
||||||
|
WebSocketSession session = new WebSocketSession(
|
||||||
|
in, rawOut, configuration.getWsFrameBufferSize());
|
||||||
|
runWsLoop(session, wsHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean keepAlive = isKeepAlive(request);
|
||||||
|
Response response = new Response(200, ContentType.TEXT_PLAIN);
|
||||||
|
|
||||||
RequestHandler handler = router.route(request);
|
RequestHandler handler = router.route(request);
|
||||||
if (handler == null) handler = router.getNotFoundHandler();
|
if (handler == null) handler = router.getNotFoundHandler();
|
||||||
@@ -140,6 +273,7 @@ class HttpServer implements ServerHandle {
|
|||||||
request.drain();
|
request.drain();
|
||||||
if (!keepAlive) break;
|
if (!keepAlive) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
if (e instanceof java.net.SocketException)
|
if (e instanceof java.net.SocketException)
|
||||||
@@ -152,18 +286,108 @@ class HttpServer implements ServerHandle {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (RejectedExecutionException ignored) {
|
} catch (RejectedExecutionException ignored) {
|
||||||
// Executor already shut down — close the socket so the client isn't left hanging.
|
|
||||||
try { socket.close(); } catch (IOException e) { log.debug("Error closing socket on shutdown", e); }
|
try { socket.close(); } catch (IOException e) { log.debug("Error closing socket on shutdown", e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WebSocket upgrade detection (zero-alloc) ──────────────────────────────
|
||||||
|
|
||||||
|
private static boolean isWebSocketUpgrade(Request request) {
|
||||||
|
ByteView upgrade = request.getRequestLine().getHeaders().view("Upgrade");
|
||||||
|
if (upgrade == null) return false;
|
||||||
|
if (!tokenEqualsIgnoreCase(upgrade, 0, upgrade.length(), "websocket")) return false;
|
||||||
|
return connectionContainsUpgrade(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean connectionContainsUpgrade(Request request) {
|
||||||
|
ByteView conn = request.getRequestLine().getHeaders().view("Connection");
|
||||||
|
if (conn == null) return false;
|
||||||
|
int len = conn.length(), i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
while (i < len && conn.byteAt(i) == ' ') i++;
|
||||||
|
int start = i;
|
||||||
|
while (i < len && conn.byteAt(i) != ',') i++;
|
||||||
|
if (tokenEqualsIgnoreCase(conn, start, i, "upgrade")) return true;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean tokenEqualsIgnoreCase(ByteView view, int start, int end, String token) {
|
||||||
|
int tlen = token.length();
|
||||||
|
int wlen = end - start;
|
||||||
|
while (wlen > 0 && view.byteAt(start + wlen - 1) == ' ') wlen--;
|
||||||
|
if (wlen != tlen) return false;
|
||||||
|
for (int i = 0; i < tlen; i++) {
|
||||||
|
byte b = view.byteAt(start + i);
|
||||||
|
if (b >= 'A' && b <= 'Z') b += 32;
|
||||||
|
if (b != (byte) token.charAt(i)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebSocket handshake ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void performHandshake(OutputStream out, Request request) throws IOException {
|
||||||
|
ByteView keyView = request.getRequestLine().getHeaders().view("Sec-WebSocket-Key");
|
||||||
|
if (keyView == null) throw new IOException("Missing Sec-WebSocket-Key header");
|
||||||
|
|
||||||
|
MessageDigest sha1 = SHA1.get();
|
||||||
|
sha1.reset();
|
||||||
|
for (int i = 0, len = keyView.length(); i < len; i++) sha1.update(keyView.byteAt(i));
|
||||||
|
sha1.update(WS_GUID_BYTES);
|
||||||
|
|
||||||
|
byte[] accept = Base64.getEncoder().encode(sha1.digest());
|
||||||
|
|
||||||
|
out.write(WS_HANDSHAKE_PREFIX);
|
||||||
|
out.write(accept);
|
||||||
|
out.write(WS_HANDSHAKE_SUFFIX);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebSocket session loop ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void runWsLoop(WebSocketSession session, WebSocketHandler handler) {
|
||||||
|
handler.onOpen(session);
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
try {
|
||||||
|
while (session.isOpen()) {
|
||||||
|
if (!session.readFrame(frame)) break;
|
||||||
|
switch (frame.opcode()) {
|
||||||
|
case WebSocketFrame.OP_TEXT, WebSocketFrame.OP_BINARY
|
||||||
|
-> handler.onMessage(session, frame);
|
||||||
|
case WebSocketFrame.OP_CLOSE
|
||||||
|
-> session.closeFromPeer(frame);
|
||||||
|
case WebSocketFrame.OP_PING
|
||||||
|
-> session.sendPong(frame);
|
||||||
|
case WebSocketFrame.OP_PONG -> { /* heartbeat ack, no-op */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
handler.onError(session, e);
|
||||||
|
} finally {
|
||||||
|
handler.onClose(session, session.closeCode());
|
||||||
|
session.forceClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP keep-alive detection ─────────────────────────────────────────────
|
||||||
|
|
||||||
private static boolean isKeepAlive(Request request) {
|
private static boolean isKeepAlive(Request request) {
|
||||||
if (request.headerEquals("Connection", "close")) return false;
|
if (request.headerEquals("Connection", "close")) return false;
|
||||||
ByteView protocol = request.getRequestLine().getProtocol();
|
ByteView protocol = request.getRequestLine().getProtocol();
|
||||||
return protocol.length() == 8 && protocol.byteAt(7) == '1'
|
int plen = protocol.length();
|
||||||
|| request.headerEquals("Connection", "keep-alive");
|
if (plen == 8) {
|
||||||
|
byte minor = protocol.byteAt(7);
|
||||||
|
if (minor == '1') return true;
|
||||||
|
if (minor == '0') return request.headerEquals("Connection", "keep-alive");
|
||||||
|
}
|
||||||
|
log.debug("Unrecognised protocol '{}', treating as close", protocol);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Response serialisation ────────────────────────────────────────────────
|
||||||
|
|
||||||
private static void writeResponse(OutputStream out, Response response, boolean keepAlive) throws IOException {
|
private static void writeResponse(OutputStream out, Response response, boolean keepAlive) throws IOException {
|
||||||
out.write(HTTP_1_1);
|
out.write(HTTP_1_1);
|
||||||
byte[] statusBytes = response.getStatusBytes();
|
byte[] statusBytes = response.getStatusBytes();
|
||||||
@@ -223,7 +447,7 @@ class HttpServer implements ServerHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void writeChunked(OutputStream out, InputStream stream) throws IOException {
|
private static void writeChunked(OutputStream out, InputStream stream) throws IOException {
|
||||||
byte[] buf = CHUNK_BUF.get();
|
byte[] buf = new byte[8192];
|
||||||
int n;
|
int n;
|
||||||
while ((n = stream.read(buf)) > 0) {
|
while ((n = stream.read(buf)) > 0) {
|
||||||
writeHex(out, n);
|
writeHex(out, n);
|
||||||
@@ -235,13 +459,16 @@ class HttpServer implements ServerHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void writeHex(OutputStream out, int value) throws IOException {
|
private static void writeHex(OutputStream out, int value) throws IOException {
|
||||||
int shift = 28;
|
int shift = 28;
|
||||||
boolean leading = true;
|
boolean leading = true;
|
||||||
while (shift >= 0) {
|
while (shift >= 0) {
|
||||||
int digit = (value >>> shift) & 0xF;
|
int digit = (value >>> shift) & 0xF;
|
||||||
if (digit != 0 || !leading) { leading = false; out.write(digit < 10 ? '0' + digit : 'a' + digit - 10); }
|
if (digit != 0 || !leading) {
|
||||||
|
leading = false;
|
||||||
|
out.write(digit < 10 ? '0' + digit : 'a' + digit - 10);
|
||||||
|
}
|
||||||
shift -= 4;
|
shift -= 4;
|
||||||
}
|
}
|
||||||
if (leading) out.write('0');
|
if (leading) out.write('0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,20 +14,39 @@ import java.net.InetSocketAddress;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One instance per connection, the buffer is allocated once and reused across keep-alive
|
* One instance per connection. The buffer is allocated once and reused across
|
||||||
* requests. Grows on demand (doubling, up to {@code maxHeaderBufferSize}). Zero String
|
* keep-alive requests. Grows on demand (doubling, up to {@code maxHeaderBufferSize}).
|
||||||
* allocations during parsing; paths, headers and protocol are exposed as {@link dev.relism.fpr.core.ByteView} slices.
|
*
|
||||||
|
* <h3>Zero-allocation design</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>No {@link String} allocations during parsing: paths, headers and
|
||||||
|
* protocol are exposed as {@link dev.relism.fpr.core.ByteView} slices
|
||||||
|
* into the shared buffer.</li>
|
||||||
|
* <li>{@link #headerMap} is reset in-place per request — single allocation
|
||||||
|
* for the lifetime of the connection.</li>
|
||||||
|
* <li>Pipelining / keep-alive leftover bytes are tracked via {@code bufBase}
|
||||||
|
* and {@code bufLen} — no copy between requests on the common path.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>State invariant</h3>
|
||||||
|
* {@code bufBase} and {@code bufLen} always reflect unconsumed bytes that belong
|
||||||
|
* to the <em>next</em> request. They are snapshotted at the top of {@link #parse}
|
||||||
|
* and reset to {@code 0/0} before any work begins, so an exception thrown mid-parse
|
||||||
|
* leaves the fields clean rather than pointing at stale data from a previous request.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RequestParser {
|
public class RequestParser {
|
||||||
private static final int INITIAL_BUFFER_SIZE = 8192;
|
private static final int INITIAL_BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
private final int maxHeaderBufferSize;
|
private final int maxHeaderBufferSize;
|
||||||
private final InetSocketAddress remoteAddress; // set once per connection, never changes
|
private final InetSocketAddress remoteAddress;
|
||||||
private final HeaderMap headerMap = new HeaderMap();
|
private final HeaderMap headerMap = new HeaderMap();
|
||||||
private byte[] buffer;
|
private byte[] buffer;
|
||||||
private int bufBase = 0; // absolute start of valid data in buffer
|
|
||||||
private int bufLen = 0; // number of valid bytes from bufBase
|
// Unconsumed bytes belonging to the NEXT request.
|
||||||
|
// Reset to 0/0 at the start of every parse() call — see invariant above.
|
||||||
|
private int bufBase = 0;
|
||||||
|
private int bufLen = 0;
|
||||||
|
|
||||||
public RequestParser() { this(64 * 1024, null); }
|
public RequestParser() { this(64 * 1024, null); }
|
||||||
public RequestParser(int maxHeaderBufferSize) { this(maxHeaderBufferSize, null); }
|
public RequestParser(int maxHeaderBufferSize) { this(maxHeaderBufferSize, null); }
|
||||||
@@ -37,9 +56,21 @@ public class RequestParser {
|
|||||||
this.buffer = new byte[Math.min(INITIAL_BUFFER_SIZE, maxHeaderBufferSize)];
|
this.buffer = new byte[Math.min(INITIAL_BUFFER_SIZE, maxHeaderBufferSize)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the next HTTP request from {@code in}.
|
||||||
|
*
|
||||||
|
* <p><b>Exception safety:</b> {@code bufBase} and {@code bufLen} are reset
|
||||||
|
* to {@code 0} before any parsing work begins. If an exception is thrown,
|
||||||
|
* the connection will be closed by the caller, so stale leftover state is
|
||||||
|
* never a problem — but the reset ensures correctness in test scenarios where
|
||||||
|
* the same parser instance is reused after an error.
|
||||||
|
*
|
||||||
|
* @return the parsed {@link Request}, or {@code null} on clean EOF.
|
||||||
|
* @throws IOException on malformed headers or I/O failure.
|
||||||
|
*/
|
||||||
public Request parse(InputStream in) throws IOException {
|
public Request parse(InputStream in) throws IOException {
|
||||||
// Take ownership of any leftover bytes from the previous request, then reset so
|
// Snapshot leftover bytes from the previous request, then reset immediately.
|
||||||
// early-returns leave the fields in a clean state.
|
// Any exception thrown below leaves bufBase/bufLen at 0 — safe state.
|
||||||
int base = bufBase;
|
int base = bufBase;
|
||||||
int totalRead = bufLen;
|
int totalRead = bufLen;
|
||||||
bufBase = 0;
|
bufBase = 0;
|
||||||
@@ -49,8 +80,8 @@ public class RequestParser {
|
|||||||
while (headerEndIdx == -1) {
|
while (headerEndIdx == -1) {
|
||||||
if (base + totalRead == buffer.length) {
|
if (base + totalRead == buffer.length) {
|
||||||
if (base > 0) {
|
if (base > 0) {
|
||||||
// Compact: slide valid data to position 0 — rare path (~every N requests
|
// Compact: slide valid data to position 0.
|
||||||
// where N = bufferSize / avgRequestSize rather than every request).
|
// Rare path (~every N requests where N ≈ bufferSize / avgRequestSize).
|
||||||
System.arraycopy(buffer, base, buffer, 0, totalRead);
|
System.arraycopy(buffer, base, buffer, 0, totalRead);
|
||||||
base = 0;
|
base = 0;
|
||||||
} else if (buffer.length >= maxHeaderBufferSize) {
|
} else if (buffer.length >= maxHeaderBufferSize) {
|
||||||
@@ -70,6 +101,8 @@ public class RequestParser {
|
|||||||
throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
|
throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Request line ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
int methodEnd = find(buffer, base, headerEndIdx, (byte) ' ');
|
int methodEnd = find(buffer, base, headerEndIdx, (byte) ' ');
|
||||||
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
|
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
|
||||||
|
|
||||||
@@ -81,20 +114,23 @@ public class RequestParser {
|
|||||||
if (pathEnd == -1) throw new IOException("Invalid request line (path)");
|
if (pathEnd == -1) throw new IOException("Invalid request line (path)");
|
||||||
|
|
||||||
int queryMark = find(buffer, pathStart, pathEnd, (byte) '?');
|
int queryMark = find(buffer, pathStart, pathEnd, (byte) '?');
|
||||||
FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(buffer, pathStart,
|
FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(buffer, pathStart,
|
||||||
queryMark != -1 ? queryMark - pathStart : pathEnd - pathStart);
|
queryMark != -1 ? queryMark - pathStart : pathEnd - pathStart);
|
||||||
FastPathViews.RequestByteView queryView = queryMark != -1
|
FastPathViews.RequestByteView queryView = queryMark != -1
|
||||||
? new FastPathViews.RequestByteView(buffer, queryMark + 1, pathEnd - queryMark - 1)
|
? new FastPathViews.RequestByteView(buffer, queryMark + 1, pathEnd - queryMark - 1)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
int protocolStart = pathEnd + 1;
|
int protocolStart = pathEnd + 1;
|
||||||
int protocolEnd = find(buffer, protocolStart, headerEndIdx, (byte) '\r');
|
int protocolEnd = find(buffer, protocolStart, headerEndIdx, (byte) '\r');
|
||||||
if (protocolEnd == -1) throw new IOException("Invalid request line (protocol)");
|
if (protocolEnd == -1) throw new IOException("Invalid request line (protocol)");
|
||||||
|
|
||||||
FastPathViews.RequestByteView protocolView = new FastPathViews.RequestByteView(buffer, protocolStart, protocolEnd - protocolStart);
|
FastPathViews.RequestByteView protocolView =
|
||||||
|
new FastPathViews.RequestByteView(buffer, protocolStart, protocolEnd - protocolStart);
|
||||||
|
|
||||||
|
// ── Headers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
int sectionStart = find(buffer, protocolEnd, headerEndIdx, (byte) '\n') + 1;
|
int sectionStart = find(buffer, protocolEnd, headerEndIdx, (byte) '\n') + 1;
|
||||||
int current = sectionStart;
|
int current = sectionStart;
|
||||||
long contentLength = 0;
|
long contentLength = 0;
|
||||||
boolean isChunked = false;
|
boolean isChunked = false;
|
||||||
|
|
||||||
@@ -118,35 +154,40 @@ public class RequestParser {
|
|||||||
|
|
||||||
headerMap.reset(buffer, sectionStart, headerEndIdx);
|
headerMap.reset(buffer, sectionStart, headerEndIdx);
|
||||||
|
|
||||||
|
// ── Body / pipelining accounting ─────────────────────────────────────
|
||||||
|
|
||||||
int bodyStart = headerEndIdx + 4;
|
int bodyStart = headerEndIdx + 4;
|
||||||
int preBufLen = (base + totalRead) - bodyStart;
|
int preBufLen = (base + totalRead) - bodyStart;
|
||||||
|
|
||||||
// Any bytes read beyond this request's body belong to the next request.
|
// Bytes read beyond this request's body belong to the next request.
|
||||||
// Store their absolute position in the buffer — no copy needed; the next parse()
|
// Store their absolute position — no copy needed; the next parse() call
|
||||||
// call will read directly from bufBase without touching the data.
|
// reads directly from bufBase without touching the data.
|
||||||
if (!isChunked && contentLength == 0 && preBufLen > 0) {
|
if (!isChunked && contentLength == 0 && preBufLen > 0) {
|
||||||
bufBase = bodyStart;
|
bufBase = bodyStart;
|
||||||
bufLen = preBufLen;
|
bufLen = preBufLen;
|
||||||
preBufLen = 0;
|
preBufLen = 0;
|
||||||
} else if (!isChunked && contentLength > 0 && preBufLen > contentLength) {
|
} else if (!isChunked && contentLength > 0 && preBufLen > contentLength) {
|
||||||
bufBase = bodyStart + (int) contentLength;
|
bufBase = bodyStart + (int) contentLength;
|
||||||
bufLen = preBufLen - (int) contentLength;
|
bufLen = preBufLen - (int) contentLength;
|
||||||
preBufLen = (int) contentLength;
|
preBufLen = (int) contentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestLine requestLine = new RequestLine(method, pathView, queryView, protocolView, headerMap);
|
RequestLine requestLine = new RequestLine(method, pathView, queryView, protocolView, headerMap);
|
||||||
|
|
||||||
if (isChunked) {
|
if (isChunked) {
|
||||||
return Request.forParsed(requestLine, new ChunkedInputStream(in, buffer, bodyStart, preBufLen), -1L, null, 0, 0, remoteAddress);
|
return Request.forParsed(requestLine,
|
||||||
|
new ChunkedInputStream(in, buffer, bodyStart, preBufLen),
|
||||||
|
-1L, null, 0, 0, remoteAddress);
|
||||||
}
|
}
|
||||||
return Request.forParsed(requestLine, in, contentLength, buffer, bodyStart, preBufLen, remoteAddress);
|
return Request.forParsed(requestLine, in, contentLength, buffer, bodyStart, preBufLen, remoteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Buffer scanning utilities (hot path — keep branch-free where possible) ──
|
||||||
|
|
||||||
private static int findEndOfHeader(byte[] buf, int from, int len) {
|
private static int findEndOfHeader(byte[] buf, int from, int len) {
|
||||||
for (int i = from; i <= len - 4; i++) {
|
for (int i = from; i <= len - 4; i++) {
|
||||||
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') {
|
if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n')
|
||||||
return i;
|
return i;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -177,4 +218,4 @@ public class RequestParser {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package dev.relism.flash;
|
|||||||
import dev.relism.flash.extension.FlashApp;
|
import dev.relism.flash.extension.FlashApp;
|
||||||
import dev.relism.flash.extension.FlashConfiguration;
|
import dev.relism.flash.extension.FlashConfiguration;
|
||||||
import dev.relism.flash.routing.AbstractRouter;
|
import dev.relism.flash.routing.AbstractRouter;
|
||||||
|
import dev.relism.flash.routing.AbstractWsRouter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -26,7 +27,9 @@ public interface ServerHandle {
|
|||||||
/** Gracefully stops the server, draining active connections. */
|
/** Gracefully stops the server, draining active connections. */
|
||||||
CompletableFuture<Void> stop();
|
CompletableFuture<Void> stop();
|
||||||
|
|
||||||
static ServerHandle create(FlashConfiguration config, AbstractRouter router) throws IOException {
|
static ServerHandle create(FlashConfiguration config,
|
||||||
return new HttpServer(config, router);
|
AbstractRouter httpRouter,
|
||||||
|
AbstractWsRouter wsRouter) throws IOException {
|
||||||
|
return new HttpServer(config, httpRouter, wsRouter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import dev.relism.flash.http.HttpMethod;
|
|||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
import dev.relism.flash.models.SimpleHandler;
|
import dev.relism.flash.models.SimpleHandler;
|
||||||
import dev.relism.flash.routing.AbstractRouter;
|
import dev.relism.flash.routing.AbstractRouter;
|
||||||
|
import dev.relism.flash.routing.AbstractWsRouter;
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
import dev.relism.flash.routing.routers.fastpathrouter.FastPathRouterImpl;
|
import dev.relism.flash.routing.routers.fastpathrouter.FastPathRouterImpl;
|
||||||
|
import dev.relism.flash.routing.routers.fastpathrouter.FastPathWsRouterImpl;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -52,16 +58,20 @@ import java.util.function.Consumer;
|
|||||||
public final class FlashApp extends FlashRegistrar<FlashApp> {
|
public final class FlashApp extends FlashRegistrar<FlashApp> {
|
||||||
|
|
||||||
private final AbstractRouter router = new FastPathRouterImpl();
|
private final AbstractRouter router = new FastPathRouterImpl();
|
||||||
|
private final AbstractWsRouter wsRouter = new FastPathWsRouterImpl();
|
||||||
private final ServerHandle server;
|
private final ServerHandle server;
|
||||||
private final FlashContext ctx = new FlashContext();
|
private final FlashContext ctx = new FlashContext();
|
||||||
private final List<FlashExtension> extensions = new ArrayList<>();
|
private final List<FlashExtension> extensions = new ArrayList<>();
|
||||||
private final List<Middleware> globalMiddlewares = new ArrayList<>();
|
private final List<Middleware> globalMiddlewares = new ArrayList<>();
|
||||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||||
|
private final List<WsRouteDefinition> deferredWsRoutes = new ArrayList<>();
|
||||||
private int port;
|
private int port;
|
||||||
|
|
||||||
|
private record WsRouteDefinition(String path, WebSocketEndpoint endpoint) {}
|
||||||
|
|
||||||
private FlashApp(FlashConfiguration config) {
|
private FlashApp(FlashConfiguration config) {
|
||||||
try {
|
try {
|
||||||
this.server = ServerHandle.create(config, router);
|
this.server = ServerHandle.create(config, router, wsRouter);
|
||||||
this.port = config.getPort();
|
this.port = config.getPort();
|
||||||
}
|
}
|
||||||
catch (IOException e) { throw new InitializationException("Failed to bind on port " + config.getPort(), e); }
|
catch (IOException e) { throw new InitializationException("Failed to bind on port " + config.getPort(), e); }
|
||||||
@@ -83,6 +93,26 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
|||||||
FlashScope scope = new FlashScope(namespace, ctx);
|
FlashScope scope = new FlashScope(namespace, ctx);
|
||||||
configure.accept(scope);
|
configure.accept(scope);
|
||||||
deferredRoutes.addAll(scope.routes());
|
deferredRoutes.addAll(scope.routes());
|
||||||
|
deferredWsRoutes.addAll(scope.wsRoutes().stream()
|
||||||
|
.map(r -> new WsRouteDefinition(r.path(), r.endpoint()))
|
||||||
|
.toList());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a WebSocket endpoint at {@code path}.
|
||||||
|
* Runs on the same virtual-thread model as HTTP handlers.
|
||||||
|
* A path may have both an HTTP handler and a WS endpoint simultaneously.
|
||||||
|
*/
|
||||||
|
public FlashApp ws(String path, WebSocketHandler handler) {
|
||||||
|
WebSocketEndpoint endpoint = handler instanceof WebSocketEndpoint e ? e
|
||||||
|
: new WebSocketEndpoint() {
|
||||||
|
@Override public void onOpen(WebSocketSession s) { handler.onOpen(s); }
|
||||||
|
@Override public void onMessage(WebSocketSession s, WebSocketFrame f) { handler.onMessage(s, f); }
|
||||||
|
@Override public void onClose(WebSocketSession s, int c) { handler.onClose(s, c); }
|
||||||
|
@Override public void onError(WebSocketSession s, Throwable t) { handler.onError(s, t); }
|
||||||
|
};
|
||||||
|
deferredWsRoutes.add(new WsRouteDefinition(path, endpoint));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +182,11 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
|||||||
!(handler instanceof SimpleHandler), ctx, "/"));
|
!(handler instanceof SimpleHandler), ctx, "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
|
||||||
|
deferredWsRoutes.add(new WsRouteDefinition(path, endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addMiddleware(Middleware mw) { globalMiddlewares.add(mw); }
|
protected void addMiddleware(Middleware mw) { globalMiddlewares.add(mw); }
|
||||||
|
|
||||||
@@ -163,6 +198,7 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
|||||||
ctx.resolveAll();
|
ctx.resolveAll();
|
||||||
extensions.forEach(e -> e.routes(this, ctx));
|
extensions.forEach(e -> e.routes(this, ctx));
|
||||||
compile();
|
compile();
|
||||||
|
compileWs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Compilation ──────────────────────────────────────────────────────────
|
// ── Compilation ──────────────────────────────────────────────────────────
|
||||||
@@ -187,6 +223,14 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void compileWs() {
|
||||||
|
for (WsRouteDefinition def : deferredWsRoutes) {
|
||||||
|
def.endpoint().bind(ctx);
|
||||||
|
emitWsEvent(def.path(), def.endpoint());
|
||||||
|
wsRouter.register(HttpMethod.GET, def.path(), def.endpoint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void emitEvent(RouteDefinition def, Middleware[] chain) {
|
private void emitEvent(RouteDefinition def, Middleware[] chain) {
|
||||||
List<RouteListener> listeners = def.ctx().routeListeners();
|
List<RouteListener> listeners = def.ctx().routeListeners();
|
||||||
@@ -198,6 +242,14 @@ public final class FlashApp extends FlashRegistrar<FlashApp> {
|
|||||||
listeners.forEach(l -> l.onRoute(event));
|
listeners.forEach(l -> l.onRoute(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void emitWsEvent(String path, WebSocketEndpoint endpoint) {
|
||||||
|
List<RouteListener> listeners = ctx.routeListeners();
|
||||||
|
if (listeners.isEmpty()) return;
|
||||||
|
RouteEvent event = new RouteEvent(HttpMethod.GET, path, "/", "FlashApp",
|
||||||
|
endpoint.getClass(), List.of());
|
||||||
|
listeners.forEach(l -> l.onRoute(event));
|
||||||
|
}
|
||||||
|
|
||||||
private static Middleware[] concat(List<Middleware> global, List<Middleware> scope,
|
private static Middleware[] concat(List<Middleware> global, List<Middleware> scope,
|
||||||
List<Middleware> injected, List<Middleware> explicit) {
|
List<Middleware> injected, List<Middleware> explicit) {
|
||||||
int total = global.size() + scope.size() + injected.size() + explicit.size();
|
int total = global.size() + scope.size() + injected.size() + explicit.size();
|
||||||
|
|||||||
@@ -28,4 +28,8 @@ public class FlashConfiguration {
|
|||||||
/** Maximum size of the request header buffer in bytes. Default: 64 KB. */
|
/** Maximum size of the request header buffer in bytes. Default: 64 KB. */
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
int maxHeaderBufferSize = 64 * 1024;
|
int maxHeaderBufferSize = 64 * 1024;
|
||||||
|
|
||||||
|
/** Per-connection WebSocket read buffer size in bytes. Default: 64 KB. */
|
||||||
|
@Builder.Default
|
||||||
|
int wsFrameBufferSize = 64 * 1024;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import dev.relism.flash.exceptions.InitializationException;
|
|||||||
import dev.relism.flash.http.HttpMethod;
|
import dev.relism.flash.http.HttpMethod;
|
||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
import dev.relism.flash.models.SimpleHandler;
|
import dev.relism.flash.models.SimpleHandler;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
import dev.relism.flash.routing.Route;
|
import dev.relism.flash.routing.Route;
|
||||||
import dev.relism.flash.routing.Routes;
|
import dev.relism.flash.routing.Routes;
|
||||||
|
import dev.relism.flash.routing.Ws;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -68,10 +70,15 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
|||||||
* class fails to load or instantiate
|
* class fails to load or instantiate
|
||||||
*/
|
*/
|
||||||
public final SELF scan(String packageName) {
|
public final SELF scan(String packageName) {
|
||||||
PackageScanner.findHandlers(packageName).forEach(cls -> {
|
PackageScanner.ScanResult found = PackageScanner.scan(packageName);
|
||||||
|
found.httpHandlers().forEach(cls -> {
|
||||||
Route ann = Routes.of(cls);
|
Route ann = Routes.of(cls);
|
||||||
addRoute(ann.method(), ann.path(), instantiate(cls), List.of());
|
addRoute(ann.method(), ann.path(), instantiate(cls), List.of());
|
||||||
});
|
});
|
||||||
|
found.wsEndpoints().forEach(cls -> {
|
||||||
|
Ws ann = cls.getAnnotation(Ws.class);
|
||||||
|
addWsRoute(ann.value(), instantiateWs(cls));
|
||||||
|
});
|
||||||
return (SELF) this;
|
return (SELF) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +94,8 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
|||||||
protected abstract void addRoute(HttpMethod method, String path,
|
protected abstract void addRoute(HttpMethod method, String path,
|
||||||
RequestHandler handler, List<Middleware> mw);
|
RequestHandler handler, List<Middleware> mw);
|
||||||
|
|
||||||
|
protected abstract void addWsRoute(String path, WebSocketEndpoint endpoint);
|
||||||
|
|
||||||
/** Registers a middleware in this registrar's own scope (global or scope-level). */
|
/** Registers a middleware in this registrar's own scope (global or scope-level). */
|
||||||
protected abstract void addMiddleware(Middleware mw);
|
protected abstract void addMiddleware(Middleware mw);
|
||||||
|
|
||||||
@@ -103,4 +112,13 @@ public abstract class FlashRegistrar<SELF extends FlashRegistrar<SELF>> {
|
|||||||
" — ensure it has a public no-arg constructor", e);
|
" — ensure it has a public no-arg constructor", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static WebSocketEndpoint instantiateWs(Class<?> cls) {
|
||||||
|
try { return (WebSocketEndpoint) cls.getDeclaredConstructor().newInstance(); }
|
||||||
|
catch (Exception e) {
|
||||||
|
throw new InitializationException(
|
||||||
|
"Failed to instantiate WS endpoint " + cls.getName() +
|
||||||
|
" — ensure public no-arg constructor", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dev.relism.flash.models.RequestHandler;
|
|||||||
import dev.relism.flash.models.SimpleHandler;
|
import dev.relism.flash.models.SimpleHandler;
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
import dev.relism.flash.routing.PathUtils;
|
import dev.relism.flash.routing.PathUtils;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,6 +30,9 @@ public final class FlashScope extends FlashRegistrar<FlashScope> {
|
|||||||
private final FlashContext ctx;
|
private final FlashContext ctx;
|
||||||
private final List<Middleware> scopeMiddlewares = new ArrayList<>();
|
private final List<Middleware> scopeMiddlewares = new ArrayList<>();
|
||||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||||
|
private final List<WsRouteDefinition> deferredWsRoutes = new ArrayList<>();
|
||||||
|
|
||||||
|
record WsRouteDefinition(String path, WebSocketEndpoint endpoint) {}
|
||||||
|
|
||||||
FlashScope(String namespace, FlashContext parentCtx) {
|
FlashScope(String namespace, FlashContext parentCtx) {
|
||||||
this.namespace = PathUtils.sanitize(namespace);
|
this.namespace = PathUtils.sanitize(namespace);
|
||||||
@@ -47,12 +51,18 @@ public final class FlashScope extends FlashRegistrar<FlashScope> {
|
|||||||
!(handler instanceof SimpleHandler), ctx, namespace));
|
!(handler instanceof SimpleHandler), ctx, namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addWsRoute(String path, WebSocketEndpoint endpoint) {
|
||||||
|
deferredWsRoutes.add(new WsRouteDefinition(ns(path), endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void addMiddleware(Middleware mw) { scopeMiddlewares.add(mw); }
|
protected void addMiddleware(Middleware mw) { scopeMiddlewares.add(mw); }
|
||||||
|
|
||||||
// ── Internal (called by FlashApp.mount) ───────────────────────────────────
|
// ── Internal (called by FlashApp.mount) ───────────────────────────────────
|
||||||
|
|
||||||
List<RouteDefinition> routes() { return deferredRoutes; }
|
List<RouteDefinition> routes() { return deferredRoutes; }
|
||||||
|
List<WsRouteDefinition> wsRoutes() { return deferredWsRoutes; }
|
||||||
|
|
||||||
private String ns(String path) { return PathUtils.join(namespace, path); }
|
private String ns(String path) { return PathUtils.join(namespace, path); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package dev.relism.flash.extension;
|
|||||||
|
|
||||||
import dev.relism.flash.exceptions.InitializationException;
|
import dev.relism.flash.exceptions.InitializationException;
|
||||||
import dev.relism.flash.models.RequestHandler;
|
import dev.relism.flash.models.RequestHandler;
|
||||||
|
import dev.relism.flash.routing.Ws;
|
||||||
import dev.relism.flash.routing.Routes;
|
import dev.relism.flash.routing.Routes;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
@@ -26,6 +29,8 @@ final class PackageScanner {
|
|||||||
|
|
||||||
private PackageScanner() {}
|
private PackageScanner() {}
|
||||||
|
|
||||||
|
record ScanResult(List<Class<?>> httpHandlers, List<Class<?>> wsEndpoints) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handlers in {@code packageName} that have {@link Routes#of(Class) resolvable} route metadata.
|
* Handlers in {@code packageName} that have {@link Routes#of(Class) resolvable} route metadata.
|
||||||
*
|
*
|
||||||
@@ -33,12 +38,17 @@ final class PackageScanner {
|
|||||||
* handler class fails to load
|
* handler class fails to load
|
||||||
*/
|
*/
|
||||||
static List<Class<?>> findHandlers(String packageName) {
|
static List<Class<?>> findHandlers(String packageName) {
|
||||||
|
return scan(packageName).httpHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ScanResult scan(String packageName) {
|
||||||
if (packageName == null || packageName.isBlank())
|
if (packageName == null || packageName.isBlank())
|
||||||
throw new InitializationException("scan() called with null or blank package name");
|
throw new InitializationException("scan() called with null or blank package name");
|
||||||
|
|
||||||
String resourcePath = packageName.replace('.', '/');
|
String resourcePath = packageName.replace('.', '/');
|
||||||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||||||
List<Class<?>> result = new ArrayList<>();
|
List<Class<?>> http = new ArrayList<>();
|
||||||
|
List<Class<?>> ws = new ArrayList<>();
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
boolean packageFound = false;
|
boolean packageFound = false;
|
||||||
|
|
||||||
@@ -49,17 +59,17 @@ final class PackageScanner {
|
|||||||
URL url = resources.nextElement();
|
URL url = resources.nextElement();
|
||||||
String protocol = url.getProtocol();
|
String protocol = url.getProtocol();
|
||||||
if ("file".equals(protocol)) {
|
if ("file".equals(protocol)) {
|
||||||
scanDirectory(new File(url.toURI()), packageName, cl, result, errors);
|
scanDirectory(new File(url.toURI()), packageName, cl, http, ws, errors);
|
||||||
} else if ("jar".equals(protocol)) {
|
} else if ("jar".equals(protocol)) {
|
||||||
String jarPath = url.getPath();
|
String jarPath = url.getPath();
|
||||||
String filePart = jarPath.substring(jarPath.indexOf("file:") + 5, jarPath.indexOf('!'));
|
String filePart = jarPath.substring(jarPath.indexOf("file:") + 5, jarPath.indexOf('!'));
|
||||||
try (JarFile jar = new JarFile(filePart)) {
|
try (JarFile jar = new JarFile(filePart)) {
|
||||||
scanJar(jar, resourcePath, packageName, cl, result, errors);
|
scanJar(jar, resourcePath, packageName, cl, http, ws, errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InitializationException e) {
|
} catch (InitializationException e) {
|
||||||
throw e; // re-throw our own exceptions
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new InitializationException("Failed to scan package: " + packageName, e);
|
throw new InitializationException("Failed to scan package: " + packageName, e);
|
||||||
}
|
}
|
||||||
@@ -74,38 +84,38 @@ final class PackageScanner {
|
|||||||
"scan(\"" + packageName + "\") — failed to load " + errors.size() + " handler(s):\n • " +
|
"scan(\"" + packageName + "\") — failed to load " + errors.size() + " handler(s):\n • " +
|
||||||
String.join("\n • ", errors));
|
String.join("\n • ", errors));
|
||||||
|
|
||||||
if (result.isEmpty())
|
if (http.isEmpty() && ws.isEmpty())
|
||||||
throw new InitializationException(
|
throw new InitializationException(
|
||||||
"scan(\"" + packageName + "\") — no routable handlers found. " +
|
"scan(\"" + packageName + "\") — no routable handlers found. " +
|
||||||
"Ensure classes extend RequestHandler, declare @Route or @GET/@POST/…, are not abstract, " +
|
"Ensure classes extend RequestHandler or WebSocketEndpoint, declare the right route annotation, " +
|
||||||
"and have a public no-arg constructor.");
|
"are not abstract, and have a public no-arg constructor.");
|
||||||
|
|
||||||
return result;
|
return new ScanResult(List.copyOf(http), List.copyOf(ws));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void scanDirectory(File dir, String packageName, ClassLoader cl,
|
private static void scanDirectory(File dir, String packageName, ClassLoader cl,
|
||||||
List<Class<?>> result, List<String> errors) {
|
List<Class<?>> http, List<Class<?>> ws, List<String> errors) {
|
||||||
File[] files = dir.listFiles();
|
File[] files = dir.listFiles();
|
||||||
if (files == null) return;
|
if (files == null) return;
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
scanDirectory(file, packageName + '.' + file.getName(), cl, result, errors);
|
scanDirectory(file, packageName + '.' + file.getName(), cl, http, ws, errors);
|
||||||
} else if (file.getName().endsWith(".class") && !isAnonymous(file.getName())) {
|
} else if (file.getName().endsWith(".class") && !isAnonymous(file.getName())) {
|
||||||
String className = packageName + '.' + file.getName().replace(".class", "");
|
String className = packageName + '.' + file.getName().replace(".class", "");
|
||||||
tryLoad(className, cl, result, errors);
|
tryLoad(className, cl, http, ws, errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void scanJar(JarFile jar, String resourcePath, String packageName,
|
private static void scanJar(JarFile jar, String resourcePath, String packageName,
|
||||||
ClassLoader cl, List<Class<?>> result, List<String> errors) {
|
ClassLoader cl, List<Class<?>> http, List<Class<?>> ws, List<String> errors) {
|
||||||
String prefix = resourcePath + "/";
|
String prefix = resourcePath + "/";
|
||||||
Enumeration<JarEntry> entries = jar.entries();
|
Enumeration<JarEntry> entries = jar.entries();
|
||||||
while (entries.hasMoreElements()) {
|
while (entries.hasMoreElements()) {
|
||||||
String name = entries.nextElement().getName();
|
String name = entries.nextElement().getName();
|
||||||
if (name.startsWith(prefix) && name.endsWith(".class") && !isAnonymous(name)) {
|
if (name.startsWith(prefix) && name.endsWith(".class") && !isAnonymous(name)) {
|
||||||
String className = name.replace('/', '.').replace(".class", "");
|
String className = name.replace('/', '.').replace(".class", "");
|
||||||
tryLoad(className, cl, result, errors);
|
tryLoad(className, cl, http, ws, errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,22 +135,25 @@ final class PackageScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void tryLoad(String className, ClassLoader cl,
|
private static void tryLoad(String className, ClassLoader cl,
|
||||||
List<Class<?>> result, List<String> errors) {
|
List<Class<?>> http, List<Class<?>> ws, List<String> errors) {
|
||||||
try {
|
try {
|
||||||
Class<?> cls = cl.loadClass(className);
|
Class<?> cls = cl.loadClass(className);
|
||||||
if (!RequestHandler.class.isAssignableFrom(cls)) return;
|
if (Modifier.isAbstract(cls.getModifiers())) return;
|
||||||
if (java.lang.reflect.Modifier.isAbstract(cls.getModifiers())) return;
|
|
||||||
if (Routes.of(cls) == null) return;
|
|
||||||
|
|
||||||
// Verify no-arg constructor exists — fail-fast if missing
|
if (RequestHandler.class.isAssignableFrom(cls)
|
||||||
try {
|
&& Routes.of(cls) != null
|
||||||
cls.getDeclaredConstructor();
|
&& !cls.isAnnotationPresent(Ws.class)) {
|
||||||
} catch (NoSuchMethodException e) {
|
assertNoArgConstructor(cls, errors);
|
||||||
errors.add(className + " — missing public no-arg constructor");
|
http.add(cls);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.add(cls);
|
if (WebSocketEndpoint.class.isAssignableFrom(cls)
|
||||||
|
&& cls.isAnnotationPresent(Ws.class)) {
|
||||||
|
assertNoArgConstructor(cls, errors);
|
||||||
|
ws.add(cls);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
errors.add(className + " — class not found: " + e.getMessage());
|
errors.add(className + " — class not found: " + e.getMessage());
|
||||||
} catch (NoClassDefFoundError e) {
|
} catch (NoClassDefFoundError e) {
|
||||||
@@ -149,4 +162,9 @@ final class PackageScanner {
|
|||||||
errors.add(className + " — linkage error: " + e.getMessage());
|
errors.add(className + " — linkage error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void assertNoArgConstructor(Class<?> cls, List<String> errors) {
|
||||||
|
try { cls.getDeclaredConstructor(); }
|
||||||
|
catch (NoSuchMethodException e) { errors.add(cls.getName() + " — missing public no-arg constructor"); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package dev.relism.flash.routing;
|
||||||
|
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
import dev.relism.flash.models.PathParams;
|
||||||
|
import dev.relism.flash.models.Request;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
|
||||||
|
public abstract class AbstractWsRouter {
|
||||||
|
|
||||||
|
public final AbstractWsRouter register(HttpMethod method, String path, WebSocketHandler handler) {
|
||||||
|
return addRoute(method, PathUtils.sanitize(path), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract WebSocketHandler route(Request request);
|
||||||
|
|
||||||
|
protected abstract AbstractWsRouter addRoute(HttpMethod method, String path, WebSocketHandler handler);
|
||||||
|
|
||||||
|
protected static void setPathParams(Request request, MatchResult<WebSocketHandler> result,
|
||||||
|
String[] allNames, int methodLen) {
|
||||||
|
int count = result.paramCount();
|
||||||
|
if (count == 0) return;
|
||||||
|
String[] names = new String[count];
|
||||||
|
int[] starts = new int[count];
|
||||||
|
int[] lens = new int[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
names[i] = allNames[result.keyIdAt(i)];
|
||||||
|
starts[i] = result.startAt(i) - methodLen;
|
||||||
|
lens[i] = result.lenAt(i);
|
||||||
|
}
|
||||||
|
PathParams.inject(request,
|
||||||
|
new PathParams(request.getRequestLine().getPath(), names, starts, lens));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dev.relism.flash.routing;
|
||||||
|
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a {@link dev.relism.flash.models.RequestHandler} subclass as a
|
||||||
|
* WebSocket endpoint. Resolved at boot time by {@link Routes#of} via the
|
||||||
|
* existing meta-annotation mechanism — no additional reflection required.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* @Ws("/chat")
|
||||||
|
* public class ChatHandler extends RequestHandler { ... }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Route(method = HttpMethod.GET, path = "")
|
||||||
|
public @interface Ws {
|
||||||
|
String value();
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package dev.relism.flash.routing.routers.fastpathrouter;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.fpr.core.FastPathRouter;
|
||||||
|
import dev.relism.fpr.core.MatchResult;
|
||||||
|
import dev.relism.fpr.core.RouterBuilder;
|
||||||
|
import dev.relism.fpr.core.dsl.StringRouteParser;
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
import dev.relism.flash.models.Request;
|
||||||
|
import dev.relism.flash.routing.AbstractWsRouter;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
|
||||||
|
public final class FastPathWsRouterImpl extends AbstractWsRouter {
|
||||||
|
|
||||||
|
private final RouterBuilder<WebSocketHandler> builder = new RouterBuilder<>();
|
||||||
|
private volatile FastPathRouter<ByteView, WebSocketHandler> router;
|
||||||
|
private String[] cachedParamNames;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractWsRouter addRoute(HttpMethod method, String path, WebSocketHandler handler) {
|
||||||
|
builder.add(StringRouteParser.parse(method.name() + path), handler);
|
||||||
|
this.router = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebSocketHandler route(Request request) {
|
||||||
|
ensureCompiled();
|
||||||
|
|
||||||
|
MatchResult<WebSocketHandler> result = Context.result();
|
||||||
|
result.reset();
|
||||||
|
|
||||||
|
HttpMethod method = request.getRequestLine().getMethod();
|
||||||
|
ByteView pathView = request.getRequestLine().getPath();
|
||||||
|
FastPathViews.MethodPathByteView combined = Context.combined();
|
||||||
|
combined.reset(method.getBytes(), pathView);
|
||||||
|
|
||||||
|
int labelId = router.match(combined, result);
|
||||||
|
if (labelId == FastPathRouter.NO_MATCH) return null;
|
||||||
|
|
||||||
|
if (result.paramCount() > 0) {
|
||||||
|
setPathParams(request, result, cachedParamNames, method.getBytes().length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureCompiled() {
|
||||||
|
if (router == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (router == null) {
|
||||||
|
cachedParamNames = builder.paramNames();
|
||||||
|
router = builder.compile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Context {
|
||||||
|
private static final ThreadLocal<MatchResult<WebSocketHandler>> RESULT =
|
||||||
|
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));
|
||||||
|
private static final ThreadLocal<FastPathViews.MethodPathByteView> COMBINED =
|
||||||
|
ThreadLocal.withInitial(FastPathViews.MethodPathByteView::new);
|
||||||
|
|
||||||
|
static MatchResult<WebSocketHandler> result() { return RESULT.get(); }
|
||||||
|
static FastPathViews.MethodPathByteView combined() { return COMBINED.get(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import dev.relism.flash.extension.FlashContext;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for class-based WebSocket endpoints registered via {@code @Ws} or
|
||||||
|
* {@code app.ws(path, endpoint)}.
|
||||||
|
*
|
||||||
|
* Lifecycle: bind() once at boot → onOpen / onMessage* / onClose | onError per connection.
|
||||||
|
* No relation to {@link dev.relism.flash.models.RequestHandler} — HTTP and WS lifecycles
|
||||||
|
* are fully separate.
|
||||||
|
*/
|
||||||
|
public abstract class WebSocketEndpoint implements WebSocketHandler {
|
||||||
|
|
||||||
|
private FlashContext ctx;
|
||||||
|
|
||||||
|
/** Called once by framework at boot. Do not call from user code. */
|
||||||
|
public final void bind(FlashContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
onInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Override to resolve and cache services before first connection. */
|
||||||
|
protected void onInit() {}
|
||||||
|
|
||||||
|
protected <T> T require(Class<T> type) {
|
||||||
|
checkBound();
|
||||||
|
return ctx.require(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> Optional<T> find(Class<T> type) {
|
||||||
|
checkBound();
|
||||||
|
return ctx.find(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkBound() {
|
||||||
|
if (ctx == null) throw new IllegalStateException(
|
||||||
|
getClass().getSimpleName() + " not bound — register via app.scan() or app.ws()");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zero-copy view over {@link WebSocketSession}'s read buffer.
|
||||||
|
* One instance allocated per connection, reset per frame — zero steady-state allocation.
|
||||||
|
*
|
||||||
|
* <p><b>Lifetime contract:</b> valid ONLY within the synchronous scope of
|
||||||
|
* {@link WebSocketHandler#onMessage(WebSocketSession, WebSocketFrame)}.
|
||||||
|
* The underlying buffer is owned by {@link WebSocketSession} and will be
|
||||||
|
* overwritten on the next frame read. Never retain a reference beyond the
|
||||||
|
* callback; use {@link #copyPayload()} when data must outlive the call.
|
||||||
|
*
|
||||||
|
* <p><b>Debug safety:</b> {@link #buffer()} is package-private to prevent
|
||||||
|
* accidental cross-boundary access. External consumers must go through
|
||||||
|
* {@link #copyPayload()} or process in-place within the callback.
|
||||||
|
*/
|
||||||
|
public final class WebSocketFrame {
|
||||||
|
|
||||||
|
public static final byte OP_CONTINUATION = 0x0;
|
||||||
|
public static final byte OP_TEXT = 0x1;
|
||||||
|
public static final byte OP_BINARY = 0x2;
|
||||||
|
public static final byte OP_CLOSE = 0x8;
|
||||||
|
public static final byte OP_PING = 0x9;
|
||||||
|
public static final byte OP_PONG = 0xA;
|
||||||
|
|
||||||
|
private byte[] buf;
|
||||||
|
private int payloadOff;
|
||||||
|
private int payloadLen;
|
||||||
|
private byte opcode;
|
||||||
|
private boolean fin;
|
||||||
|
|
||||||
|
// Monotonically incremented on every reset. Allows callers that hold a
|
||||||
|
// reference beyond onMessage() to detect stale access in assertions/tests.
|
||||||
|
private int generation;
|
||||||
|
|
||||||
|
/** Called by {@link WebSocketSession} only. */
|
||||||
|
void reset(byte[] buf, int off, int len, byte opcode, boolean fin) {
|
||||||
|
this.buf = buf;
|
||||||
|
this.payloadOff = off;
|
||||||
|
this.payloadLen = len;
|
||||||
|
this.opcode = opcode;
|
||||||
|
this.fin = fin;
|
||||||
|
this.generation++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte opcode() { return opcode; }
|
||||||
|
public boolean isFin() { return fin; }
|
||||||
|
public int payloadOffset() { return payloadOff; }
|
||||||
|
public int payloadLength() { return payloadLen; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the generation counter at the moment of this call.
|
||||||
|
* Store it at callback entry and compare later to detect stale retention:
|
||||||
|
* <pre>{@code
|
||||||
|
* int gen = frame.generation();
|
||||||
|
* executor.submit(() -> {
|
||||||
|
* assert frame.generation() == gen : "frame buffer was recycled!";
|
||||||
|
* });
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public int generation() { return generation; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct reference to the session read buffer — package-private to prevent
|
||||||
|
* accidental retention outside the websocket package.
|
||||||
|
* External callers: use {@link #copyPayload()}.
|
||||||
|
*/
|
||||||
|
byte[] buffer() { return buf; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the payload into a fresh array. Allocates — O(n) in payload size.
|
||||||
|
* Use only when data must outlive the {@code onMessage} callback or be
|
||||||
|
* handed off to another thread. For payloads > a few KB consider wrapping
|
||||||
|
* the result in a pooled {@link java.nio.ByteBuffer} to avoid GC pressure.
|
||||||
|
*/
|
||||||
|
public byte[] copyPayload() {
|
||||||
|
byte[] copy = new byte[payloadLen];
|
||||||
|
System.arraycopy(buf, payloadOff, copy, 0, payloadLen);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for a WebSocket endpoint. Lifecycle:
|
||||||
|
* onOpen → onMessage* → onClose | onError.
|
||||||
|
*
|
||||||
|
* Lifetime contract for {@link WebSocketFrame}: valid ONLY within
|
||||||
|
* the synchronous scope of {@link #onMessage}. Do not retain references
|
||||||
|
* to the frame or its buffer across calls — copy with {@link WebSocketFrame#copyPayload()}
|
||||||
|
* if data must outlive the callback.
|
||||||
|
*
|
||||||
|
* OP_CONTINUATION frames are delivered as-is; fragment reassembly is an
|
||||||
|
* application concern.
|
||||||
|
*/
|
||||||
|
public interface WebSocketHandler {
|
||||||
|
void onOpen(WebSocketSession session);
|
||||||
|
void onMessage(WebSocketSession session, WebSocketFrame frame);
|
||||||
|
default void onClose(WebSocketSession session, int code) {}
|
||||||
|
default void onError(WebSocketSession session, Throwable t) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-connection WebSocket I/O state. One instance per virtual thread.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* <p>With {@code TCP_NODELAY} enabled on the socket (set in {@code HttpServer}),
|
||||||
|
* Nagle's algorithm is disabled: the kernel sends data as soon as it lands in
|
||||||
|
* the send buffer, without waiting. {@link java.io.BufferedOutputStream} will
|
||||||
|
* still batch multiple small writes into one syscall when they happen in the
|
||||||
|
* same virtual-thread scheduling quantum — giving us coalescing where it's
|
||||||
|
* free, and immediate delivery where it matters.
|
||||||
|
*
|
||||||
|
* <p>The only place an explicit flush is still needed is after the WS
|
||||||
|
* handshake (one-time, not on the hot path) and after the CLOSE frame
|
||||||
|
* (end of session). Both are handled in {@link #close} and in
|
||||||
|
* {@code HttpServer#performHandshake}.</li>
|
||||||
|
*
|
||||||
|
* <li><b>Flush on CLOSE frame</b>: {@link #close} still flushes explicitly
|
||||||
|
* because the CLOSE frame is the last thing written before the stream is
|
||||||
|
* abandoned — without a flush, the 4 bytes could sit in the buffer forever.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Thread safety</h3>
|
||||||
|
* {@link #sendText}, {@link #send}, and {@link #close} are synchronized on
|
||||||
|
* {@code out} and safe to call from threads other than the session loop.
|
||||||
|
* {@link #close} uses a CAS on {@code open} to guarantee exactly-once CLOSE
|
||||||
|
* frame emission under concurrent calls.
|
||||||
|
*/
|
||||||
|
public final class WebSocketSession {
|
||||||
|
|
||||||
|
private final InputStream in;
|
||||||
|
private final OutputStream out;
|
||||||
|
private final byte[] readBuf;
|
||||||
|
|
||||||
|
private final AtomicBoolean open = new AtomicBoolean(true);
|
||||||
|
private int closeCode = 1000;
|
||||||
|
|
||||||
|
private final byte[] hdrScratch = new byte[10];
|
||||||
|
|
||||||
|
public WebSocketSession(InputStream in, OutputStream out, int bufferSize) {
|
||||||
|
this.in = in;
|
||||||
|
this.out = out;
|
||||||
|
this.readBuf = new byte[bufferSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() { return open.get(); }
|
||||||
|
public int closeCode() { return closeCode; }
|
||||||
|
|
||||||
|
// ── Public send API ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void sendText(byte[] utf8, int off, int len) throws IOException {
|
||||||
|
writeFrame(WebSocketFrame.OP_TEXT, utf8, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(byte[] payload, int off, int len) throws IOException {
|
||||||
|
writeFrame(WebSocketFrame.OP_BINARY, payload, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a CLOSE frame exactly once.
|
||||||
|
* No flush needed — {@code out} is the raw socket OutputStream (not buffered);
|
||||||
|
* each write goes directly to the kernel send buffer, and TCP_NODELAY ensures
|
||||||
|
* it's transmitted immediately.
|
||||||
|
*/
|
||||||
|
public void close(int code) throws IOException {
|
||||||
|
if (!open.compareAndSet(true, false)) return;
|
||||||
|
synchronized (out) {
|
||||||
|
out.write(0x88);
|
||||||
|
out.write(0x02);
|
||||||
|
out.write((code >> 8) & 0xFF);
|
||||||
|
out.write(code & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session loop internals ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public boolean readFrame(WebSocketFrame frame) throws IOException {
|
||||||
|
int b0 = in.read();
|
||||||
|
if (b0 < 0) return false;
|
||||||
|
int b1 = in.read();
|
||||||
|
if (b1 < 0) return false;
|
||||||
|
|
||||||
|
boolean fin = (b0 & 0x80) != 0;
|
||||||
|
byte opcode = (byte) (b0 & 0x0F);
|
||||||
|
boolean masked = (b1 & 0x80) != 0;
|
||||||
|
long payLen = (b1 & 0x7F);
|
||||||
|
|
||||||
|
if (payLen == 126) {
|
||||||
|
payLen = ((in.read() & 0xFF) << 8) | (in.read() & 0xFF);
|
||||||
|
} else if (payLen == 127) {
|
||||||
|
payLen = 0;
|
||||||
|
for (int i = 0; i < 8; i++) payLen = (payLen << 8) | (in.read() & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payLen > readBuf.length) throw new IOException(
|
||||||
|
"WS frame payload " + payLen + " bytes exceeds buffer " + readBuf.length);
|
||||||
|
|
||||||
|
byte m0 = 0, m1 = 0, m2 = 0, m3 = 0;
|
||||||
|
if (masked) {
|
||||||
|
m0 = (byte) in.read(); m1 = (byte) in.read();
|
||||||
|
m2 = (byte) in.read(); m3 = (byte) in.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = (int) payLen;
|
||||||
|
readFully(readBuf, 0, len);
|
||||||
|
|
||||||
|
if (masked) unmaskInPlace(readBuf, 0, len, m0, m1, m2, m3);
|
||||||
|
|
||||||
|
frame.reset(readBuf, 0, len, opcode, fin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPong(WebSocketFrame ping) throws IOException {
|
||||||
|
writeFrame(WebSocketFrame.OP_PONG, ping.buffer(), ping.payloadOffset(), ping.payloadLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void echo(WebSocketFrame frame) throws IOException {
|
||||||
|
if (frame.opcode() == WebSocketFrame.OP_TEXT)
|
||||||
|
sendText(frame.buffer(), frame.payloadOffset(), frame.payloadLength());
|
||||||
|
else
|
||||||
|
send(frame.buffer(), frame.payloadOffset(), frame.payloadLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeFromPeer(WebSocketFrame frame) {
|
||||||
|
if (frame.payloadLength() >= 2) {
|
||||||
|
byte[] b = frame.copyPayload();
|
||||||
|
int o = frame.payloadOffset();
|
||||||
|
closeCode = ((b[o] & 0xFF) << 8) | (b[o + 1] & 0xFF);
|
||||||
|
}
|
||||||
|
open.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceClose() {
|
||||||
|
open.set(false);
|
||||||
|
try { in.close(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the WS frame header into {@link #hdrScratch} (at most 10 bytes),
|
||||||
|
* then writes header + payload in two bulk calls to the raw socket stream.
|
||||||
|
*
|
||||||
|
* <p>No {@code flush()} — {@code out} is the unbuffered socket {@link OutputStream}
|
||||||
|
* (see {@code HttpServer#process}). Each {@code write()} lands directly in the
|
||||||
|
* kernel send buffer. With {@code TCP_NODELAY} set on the socket, the kernel
|
||||||
|
* transmits the segment immediately without Nagle coalescing. The two writes
|
||||||
|
* (header then payload) will be merged into a single TCP segment by the kernel
|
||||||
|
* because they arrive faster than the ACK from the peer — exactly the coalescing
|
||||||
|
* we want, at zero cost.
|
||||||
|
*/
|
||||||
|
private void writeFrame(byte opcode, byte[] payload, int off, int len) throws IOException {
|
||||||
|
synchronized (out) {
|
||||||
|
int hlen = 0;
|
||||||
|
hdrScratch[hlen++] = (byte) (0x80 | opcode);
|
||||||
|
if (len <= 125) {
|
||||||
|
hdrScratch[hlen++] = (byte) len;
|
||||||
|
} else if (len <= 0xFFFF) {
|
||||||
|
hdrScratch[hlen++] = 126;
|
||||||
|
hdrScratch[hlen++] = (byte) ((len >> 8) & 0xFF);
|
||||||
|
hdrScratch[hlen++] = (byte) (len & 0xFF);
|
||||||
|
} else {
|
||||||
|
hdrScratch[hlen++] = 127;
|
||||||
|
hdrScratch[hlen++] = 0; hdrScratch[hlen++] = 0;
|
||||||
|
hdrScratch[hlen++] = 0; hdrScratch[hlen++] = 0;
|
||||||
|
hdrScratch[hlen++] = (byte) ((len >> 24) & 0xFF);
|
||||||
|
hdrScratch[hlen++] = (byte) ((len >> 16) & 0xFF);
|
||||||
|
hdrScratch[hlen++] = (byte) ((len >> 8) & 0xFF);
|
||||||
|
hdrScratch[hlen++] = (byte) (len & 0xFF);
|
||||||
|
}
|
||||||
|
out.write(hdrScratch, 0, hlen);
|
||||||
|
out.write(payload, off, len);
|
||||||
|
// No flush — TCP_NODELAY handles delivery. See Javadoc above.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFully(byte[] buf, int off, int len) throws IOException {
|
||||||
|
int remaining = len;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int n = in.read(buf, off + (len - remaining), remaining);
|
||||||
|
if (n < 0) throw new EOFException("WebSocket stream closed mid-frame");
|
||||||
|
remaining -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void unmaskInPlace(byte[] buf, int off, int len,
|
||||||
|
byte m0, byte m1, byte m2, byte m3) {
|
||||||
|
int i = off;
|
||||||
|
int end = off + len;
|
||||||
|
int end4 = off + (len & ~3);
|
||||||
|
while (i < end4) {
|
||||||
|
buf[i] ^= m0;
|
||||||
|
buf[i+1] ^= m1;
|
||||||
|
buf[i+2] ^= m2;
|
||||||
|
buf[i+3] ^= m3;
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
if (i < end) { buf[i++] ^= m0; }
|
||||||
|
if (i < end) { buf[i++] ^= m1; }
|
||||||
|
if (i < end) { buf[i] ^= m2; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package dev.relism.flash;
|
||||||
|
|
||||||
|
import dev.relism.flash.extension.FlashApp;
|
||||||
|
import dev.relism.flash.extension.FlashConfiguration;
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class HttpServerWebSocketTest {
|
||||||
|
|
||||||
|
private FlashApp app;
|
||||||
|
private int port;
|
||||||
|
private final AtomicReference<Integer> closed = new AtomicReference<>();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
try (ServerSocket s = new ServerSocket(0)) {
|
||||||
|
port = s.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
app = FlashApp.create(FlashConfiguration.builder()
|
||||||
|
.port(port)
|
||||||
|
.host("127.0.0.1")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
app.ws("/chat", new WebSocketHandler() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocketSession session) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocketSession session, WebSocketFrame frame) {
|
||||||
|
try {
|
||||||
|
if (frame.opcode() == WebSocketFrame.OP_TEXT) {
|
||||||
|
session.sendText(frame.copyPayload(), frame.payloadOffset(), frame.payloadLength());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.ws("/close", new WebSocketHandler() {
|
||||||
|
@Override public void onOpen(WebSocketSession session) {}
|
||||||
|
@Override public void onMessage(WebSocketSession session, WebSocketFrame frame) {}
|
||||||
|
@Override public void onClose(WebSocketSession session, int code) { closed.set(code); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (app != null) app.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int SOCKET_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
private static String handshakeKey() {
|
||||||
|
return Base64.getEncoder().encodeToString("flash-test-key".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readHeaders(InputStream in) throws Exception {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
int b, prev3 = -1, prev2 = -1, prev1 = -1;
|
||||||
|
while ((b = in.read()) != -1) {
|
||||||
|
out.write(b);
|
||||||
|
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
|
||||||
|
prev3 = prev2; prev2 = prev1; prev1 = b;
|
||||||
|
}
|
||||||
|
return out.toString(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] textFrame(String message) {
|
||||||
|
byte[] payload = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] frame = new byte[payload.length + 6];
|
||||||
|
frame[0] = (byte) 0x81;
|
||||||
|
frame[1] = (byte) (0x80 | payload.length);
|
||||||
|
byte[] mask = {1, 2, 3, 4};
|
||||||
|
System.arraycopy(mask, 0, frame, 2, 4);
|
||||||
|
for (int i = 0; i < payload.length; i++) {
|
||||||
|
frame[6 + i] = (byte) (payload[i] ^ mask[i & 3]);
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] closeFrame(int code) {
|
||||||
|
byte[] frame = new byte[8];
|
||||||
|
frame[0] = (byte) 0x88;
|
||||||
|
frame[1] = (byte) 0x82;
|
||||||
|
byte[] mask = {1, 2, 3, 4};
|
||||||
|
System.arraycopy(mask, 0, frame, 2, 4);
|
||||||
|
frame[6] = (byte) (((code >> 8) & 0xFF) ^ mask[0]);
|
||||||
|
frame[7] = (byte) ((code & 0xFF) ^ mask[1]);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void websocket_upgrade_returns101AndEchoesText() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port);
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
OutputStream out = socket.getOutputStream()) {
|
||||||
|
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||||
|
|
||||||
|
String key = handshakeKey();
|
||||||
|
String req = "GET /chat HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Upgrade: websocket\r\n" +
|
||||||
|
"Connection: keep-alive, Upgrade\r\n" +
|
||||||
|
"Sec-WebSocket-Key: " + key + "\r\n" +
|
||||||
|
"Sec-WebSocket-Version: 13\r\n\r\n";
|
||||||
|
out.write(req.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String headers = readHeaders(in);
|
||||||
|
assertTrue(headers.startsWith("HTTP/1.1 101 Switching Protocols"));
|
||||||
|
assertTrue(headers.contains("Upgrade: websocket"));
|
||||||
|
assertTrue(headers.contains("Connection: Upgrade"));
|
||||||
|
assertTrue(headers.contains("Sec-WebSocket-Accept: "));
|
||||||
|
|
||||||
|
out.write(textFrame("hello"));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
byte[] frame = in.readNBytes(7);
|
||||||
|
assertEquals((byte) 0x81, frame[0]);
|
||||||
|
assertEquals((byte) 0x05, frame[1]);
|
||||||
|
assertEquals("hello", new String(frame, 2, 5, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void websocket_ping_is_ponged() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port);
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
OutputStream out = socket.getOutputStream()) {
|
||||||
|
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||||
|
|
||||||
|
out.write(("GET /chat HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Upgrade: websocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"Sec-WebSocket-Key: " + handshakeKey() + "\r\n" +
|
||||||
|
"Sec-WebSocket-Version: 13\r\n\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
readHeaders(in);
|
||||||
|
|
||||||
|
out.write(new byte[] {(byte) 0x89, (byte) 0x80, 1, 2, 3, 4});
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
byte[] pong = in.readNBytes(2);
|
||||||
|
assertEquals((byte) 0x8A, pong[0]);
|
||||||
|
assertEquals((byte) 0x00, pong[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void websocket_close_frame_closesSession() throws Exception {
|
||||||
|
try (Socket socket = new Socket("127.0.0.1", port);
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
OutputStream out = socket.getOutputStream()) {
|
||||||
|
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||||
|
|
||||||
|
out.write(("GET /close HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Upgrade: websocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"Sec-WebSocket-Key: " + handshakeKey() + "\r\n" +
|
||||||
|
"Sec-WebSocket-Version: 13\r\n\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
readHeaders(in);
|
||||||
|
|
||||||
|
out.write(closeFrame(1000));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
Thread.sleep(100);
|
||||||
|
assertEquals(1000, closed.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package dev.relism.flash.extension;
|
||||||
|
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class FlashAppWebSocketTest {
|
||||||
|
|
||||||
|
private FlashApp app;
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
try (ServerSocket s = new ServerSocket(0)) {
|
||||||
|
port = s.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
app = FlashApp.create(FlashConfiguration.builder()
|
||||||
|
.port(port)
|
||||||
|
.host("127.0.0.1")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (app != null) app.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ws_registersDirectEndpoint() {
|
||||||
|
WebSocketHandler handler = new WebSocketHandler() {
|
||||||
|
public void onOpen(WebSocketSession session) {}
|
||||||
|
public void onMessage(WebSocketSession session, WebSocketFrame frame) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertSame(app, app.ws("/chat", handler));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.relism.flash.extension;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class PackageScannerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scan_separatesHttpHandlersAndWsEndpoints() {
|
||||||
|
PackageScanner.ScanResult result = PackageScanner.scan("dev.relism.flash.websocket.onlyws");
|
||||||
|
|
||||||
|
assertTrue(result.wsEndpoints().stream().anyMatch(c -> c.getSimpleName().equals("OnlyWsEndpoint")));
|
||||||
|
assertFalse(result.httpHandlers().stream().anyMatch(c -> c.getSimpleName().equals("OnlyWsEndpoint")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scan_includesHttpHandlerAndWsEndpointFromTestPackage() {
|
||||||
|
PackageScanner.ScanResult result = PackageScanner.scan("dev.relism.flash.websocket.scantest");
|
||||||
|
|
||||||
|
assertTrue(result.httpHandlers().stream().anyMatch(c -> c.getSimpleName().equals("ScanHttpHandler")));
|
||||||
|
assertTrue(result.wsEndpoints().stream().anyMatch(c -> c.getSimpleName().equals("ScanWsEndpoint")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.relism.flash.routing;
|
||||||
|
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
import dev.relism.flash.models.Request;
|
||||||
|
import dev.relism.flash.websocket.WebSocketHandler;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class AbstractWsRouterTest {
|
||||||
|
|
||||||
|
static class DummyWsRouter extends AbstractWsRouter {
|
||||||
|
WebSocketHandler lastHandler;
|
||||||
|
HttpMethod lastMethod;
|
||||||
|
String lastPath;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebSocketHandler route(Request request) { return null; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractWsRouter addRoute(HttpMethod method, String path, WebSocketHandler handler) {
|
||||||
|
this.lastMethod = method;
|
||||||
|
this.lastPath = path;
|
||||||
|
this.lastHandler = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void register_sanitizesPathAndStoresHandler() {
|
||||||
|
DummyWsRouter router = new DummyWsRouter();
|
||||||
|
WebSocketHandler handler = new WebSocketHandler() {
|
||||||
|
public void onOpen(dev.relism.flash.websocket.WebSocketSession session) {}
|
||||||
|
public void onMessage(dev.relism.flash.websocket.WebSocketSession session, dev.relism.flash.websocket.WebSocketFrame frame) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.register(HttpMethod.GET, "chat/", handler);
|
||||||
|
|
||||||
|
assertEquals(HttpMethod.GET, router.lastMethod);
|
||||||
|
assertEquals("/chat", router.lastPath);
|
||||||
|
assertSame(handler, router.lastHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import dev.relism.flash.extension.FlashContext;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class WebSocketEndpointTest {
|
||||||
|
|
||||||
|
static class DummyEndpoint extends WebSocketEndpoint {
|
||||||
|
boolean initCalled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onInit() {
|
||||||
|
initCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocketSession session) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocketSession session, WebSocketFrame frame) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bind_callsOnInit() {
|
||||||
|
DummyEndpoint endpoint = new DummyEndpoint();
|
||||||
|
|
||||||
|
endpoint.bind(new FlashContext());
|
||||||
|
|
||||||
|
assertTrue(endpoint.initCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requireBeforeBind_throws() {
|
||||||
|
DummyEndpoint endpoint = new DummyEndpoint();
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> endpoint.require(String.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class WebSocketFrameTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void copyPayload_copiesActiveSlice() {
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
byte[] buf = "hello".getBytes();
|
||||||
|
frame.reset(buf, 1, 3, WebSocketFrame.OP_TEXT, true);
|
||||||
|
|
||||||
|
byte[] copy = frame.copyPayload();
|
||||||
|
|
||||||
|
assertArrayEquals("ell".getBytes(), copy);
|
||||||
|
assertNotSame(buf, copy);
|
||||||
|
assertEquals(WebSocketFrame.OP_TEXT, frame.opcode());
|
||||||
|
assertTrue(frame.isFin());
|
||||||
|
assertEquals(1, frame.payloadOffset());
|
||||||
|
assertEquals(3, frame.payloadLength());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class WebSocketSessionFrameTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readFrame_unmasksMaskedPayload() throws Exception {
|
||||||
|
byte[] raw = new byte[] {
|
||||||
|
(byte) 0x81,
|
||||||
|
(byte) 0x85,
|
||||||
|
1, 2, 3, 4,
|
||||||
|
(byte) ('h' ^ 1),
|
||||||
|
(byte) ('i' ^ 2),
|
||||||
|
(byte) ('!' ^ 3),
|
||||||
|
(byte) ('!' ^ 4),
|
||||||
|
(byte) ('?' ^ 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
WebSocketSession session = new WebSocketSession(new ByteArrayInputStream(raw), new ByteArrayOutputStream(), 16);
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
|
||||||
|
assertTrue(session.readFrame(frame));
|
||||||
|
assertEquals(WebSocketFrame.OP_TEXT, frame.opcode());
|
||||||
|
assertTrue(frame.isFin());
|
||||||
|
assertEquals(5, frame.payloadLength());
|
||||||
|
assertEquals("hi!!?", new String(frame.copyPayload(), frame.payloadOffset(), frame.payloadLength()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readFrame_rejectsOversizedPayload() {
|
||||||
|
byte[] raw = new byte[] {(byte) 0x82, (byte) 0x7E, 0x01, 0x00};
|
||||||
|
WebSocketSession session = new WebSocketSession(new ByteArrayInputStream(raw), new ByteArrayOutputStream(), 8);
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
|
||||||
|
assertThrows(Exception.class, () -> session.readFrame(frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package dev.relism.flash.websocket;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class WebSocketSessionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void close_setsClosedAndWritesFrame() throws Exception {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
WebSocketSession session = new WebSocketSession(new ByteArrayInputStream(new byte[0]), out, 64);
|
||||||
|
|
||||||
|
session.close(1000);
|
||||||
|
|
||||||
|
assertFalse(session.isOpen());
|
||||||
|
assertEquals(1000, session.closeCode());
|
||||||
|
byte[] bytes = out.toByteArray();
|
||||||
|
assertEquals((byte) 0x88, bytes[0]);
|
||||||
|
assertEquals((byte) 0x02, bytes[1]);
|
||||||
|
assertEquals((byte) 0x03, bytes[2]);
|
||||||
|
assertEquals((byte) 0xE8, bytes[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closeFromPeer_extractsCloseCode() {
|
||||||
|
WebSocketSession session = new WebSocketSession(new ByteArrayInputStream(new byte[0]), new ByteArrayOutputStream(), 64);
|
||||||
|
WebSocketFrame frame = new WebSocketFrame();
|
||||||
|
frame.reset(new byte[] {(byte) 0x03, (byte) 0xE8}, 0, 2, WebSocketFrame.OP_CLOSE, true);
|
||||||
|
|
||||||
|
session.closeFromPeer(frame);
|
||||||
|
|
||||||
|
assertFalse(session.isOpen());
|
||||||
|
assertEquals(1000, session.closeCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.relism.flash.websocket.onlyws;
|
||||||
|
|
||||||
|
import dev.relism.flash.routing.Ws;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
|
|
||||||
|
@Ws("/only")
|
||||||
|
public class OnlyWsEndpoint extends WebSocketEndpoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocketSession session) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocketSession session, WebSocketFrame frame) {
|
||||||
|
try {
|
||||||
|
session.sendText(frame.copyPayload(), frame.payloadOffset(), frame.payloadLength());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dev.relism.flash.websocket.scantest;
|
||||||
|
|
||||||
|
import dev.relism.flash.http.HttpMethod;
|
||||||
|
import dev.relism.flash.models.Request;
|
||||||
|
import dev.relism.flash.models.RequestHandler;
|
||||||
|
import dev.relism.flash.models.Response;
|
||||||
|
import dev.relism.flash.routing.GET;
|
||||||
|
|
||||||
|
@GET("/http")
|
||||||
|
public class ScanHttpHandler extends RequestHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object handle(Request request, Response response) {
|
||||||
|
return "http";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.relism.flash.websocket.scantest;
|
||||||
|
|
||||||
|
import dev.relism.flash.routing.Ws;
|
||||||
|
import dev.relism.flash.websocket.WebSocketEndpoint;
|
||||||
|
import dev.relism.flash.websocket.WebSocketFrame;
|
||||||
|
import dev.relism.flash.websocket.WebSocketSession;
|
||||||
|
|
||||||
|
@Ws("/ws")
|
||||||
|
public class ScanWsEndpoint extends WebSocketEndpoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocketSession session) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocketSession session, WebSocketFrame frame) {
|
||||||
|
try {
|
||||||
|
session.sendText(frame.copyPayload(), frame.payloadOffset(), frame.payloadLength());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# WARNING: Do not store sensitive information in this file, #
|
||||||
|
# as its contents will be included in the Qodana report. #
|
||||||
|
#################################################################################
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
projectJDK: "21" #(Applied in CI/CD pipeline)
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||||
|
# severityThresholds - configures maximum thresholds for different problem severities
|
||||||
|
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||||
|
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||||
|
#failureConditions:
|
||||||
|
# severityThresholds:
|
||||||
|
# any: 15
|
||||||
|
# critical: 5
|
||||||
|
# testCoverageThresholds:
|
||||||
|
# fresh: 70
|
||||||
|
# total: 50
|
||||||
|
|
||||||
|
#Qodana supports other languages, for example, Python, JavaScript, TypeScript, Go, C#, PHP
|
||||||
|
#For all supported languages see https://www.jetbrains.com/help/qodana/linters.html
|
||||||
|
linter: jetbrains/qodana-jvm-community:2025.3
|
||||||
Reference in New Issue
Block a user