Skip to content

Commit 2df0aa8

Browse files
mcollovatimshabarov
authored andcommitted
feat: allow production build with watermark (#21866)
Adds a configuration that allows to build watermarked applications with commercial components when no license key can be found.
1 parent 01536db commit 2df0aa8

File tree

23 files changed

+1029
-57
lines changed

23 files changed

+1029
-57
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.gradle
18+
19+
import java.io.File
20+
import java.nio.file.Files
21+
import java.nio.file.StandardCopyOption
22+
import kotlin.test.assertContains
23+
import kotlin.test.assertTrue
24+
import com.vaadin.flow.gradle.AbstractGradleTest
25+
import com.vaadin.flow.gradle.expectTaskOutcome
26+
import org.gradle.testkit.runner.TaskOutcome
27+
import org.junit.AfterClass
28+
import org.junit.Before
29+
import org.junit.BeforeClass
30+
import org.junit.ClassRule
31+
import org.junit.Test
32+
import org.junit.rules.TemporaryFolder
33+
34+
class BuildWithoutLicenseTest : AbstractGradleTest() {
35+
36+
lateinit var buildInfo: File
37+
38+
@Before
39+
fun setup() {
40+
buildInfo = testProject.newFile("output-flow-build-info.json")
41+
testProject.buildFile.writeText(
42+
"""
43+
plugins {
44+
id 'war'
45+
id 'com.vaadin.flow'
46+
}
47+
repositories {
48+
maven {
49+
url = '${realUserHome}/.m2/repository'
50+
}
51+
mavenLocal()
52+
mavenCentral()
53+
maven { url = 'https://maven.vaadin.com/vaadin-prereleases' }
54+
flatDir {
55+
dirs("libs")
56+
}
57+
}
58+
dependencies {
59+
implementation("com.vaadin:flow:$flowVersion")
60+
implementation name:'commercial-addon'
61+
providedCompile("jakarta.servlet:jakarta.servlet-api:6.0.0")
62+
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
63+
}
64+
65+
// Copy the flow-build-info.json so that tests can assert on it
66+
// after the build.
67+
tasks.named('vaadinBuildFrontend').configure {
68+
doLast {
69+
def mainResourcesDir = project.sourceSets.main.output.resourcesDir
70+
71+
// Define source file path based on the resources directory
72+
def sourceFile = new File(mainResourcesDir, "META-INF/VAADIN/config/flow-build-info.json")
73+
74+
if (sourceFile.exists()) {
75+
def destFile = project.file("${buildInfo.absolutePath}")
76+
destFile.text = sourceFile.text
77+
78+
logger.lifecycle("Copied flow-build-info.json to temporary file: ${buildInfo.absolutePath}")
79+
} else {
80+
logger.warn("Could not find flow-build-info.json to copy")
81+
}
82+
}
83+
}
84+
"""
85+
)
86+
// Add a dependency with commercial marker to trigger license validation
87+
testProject.newFolder("libs")
88+
val commercialAddonJar: File =
89+
testProject.newFile("libs/commercial-addon.jar")
90+
Files.copy(
91+
File(javaClass.classLoader.getResource("commercial-addon.jar")!!.path).toPath(),
92+
commercialAddonJar.toPath(), StandardCopyOption.REPLACE_EXISTING
93+
)
94+
}
95+
96+
@Test
97+
fun testBuildFrontendInProductionMode_buildFails() {
98+
99+
val result = testProject.buildAndFail(
100+
"-Duser.home=${testingHomeFolder}",
101+
"-Pvaadin.productionMode",
102+
"vaadinBuildFrontend"
103+
)
104+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.FAILED)
105+
assertContains(
106+
result.output,
107+
"Commercial features require a subscription."
108+
)
109+
assertContains(result.output, "* vaadin-commercial-component")
110+
assertContains(result.output, "commercialWithWatermark")
111+
}
112+
113+
@Test
114+
fun testBuildFrontendInProductionMode_watermarkBuildDisabled_buildFails() {
115+
testProject.buildFile.appendText(
116+
"""
117+
vaadin {
118+
commercialWithWatermark = false
119+
}
120+
""".trimIndent()
121+
)
122+
val result = testProject.buildAndFail(
123+
"-Duser.home=${testingHomeFolder}",
124+
"-Pvaadin.productionMode",
125+
"vaadinBuildFrontend"
126+
)
127+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.FAILED)
128+
assertContains(
129+
result.output,
130+
"Commercial features require a subscription."
131+
)
132+
assertContains(result.output, "* vaadin-commercial-component")
133+
assertContains(result.output, "commercialWithWatermark")
134+
}
135+
136+
@Test
137+
fun testBuildFrontendInProductionMode_watermarkBuildEnabledBySystemProperty_buildSucceeds() {
138+
139+
val result = testProject.build(
140+
"-Duser.home=${testingHomeFolder}",
141+
"-DcommercialWithWatermark",
142+
"-Pvaadin.productionMode",
143+
"vaadinBuildFrontend"
144+
)
145+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
146+
assertContains(result.output, "Application watermark enabled")
147+
148+
assertTrue { buildInfo.exists() }
149+
val buildInfoJson = buildInfo.readText()
150+
assertContains(
151+
buildInfoJson,
152+
Regex("(?s).*\"watermark\\.enable\"\\s*:\\s*true.*"),
153+
"watermark.enable token missing or incorrect in ${buildInfo.absolutePath}: ${buildInfoJson}"
154+
)
155+
}
156+
157+
@Test
158+
fun testBuildFrontendInProductionMode_watermarkBuildEnabled_buildSucceeds() {
159+
testProject.buildFile.appendText(
160+
"""
161+
vaadin {
162+
commercialWithWatermark = true
163+
}
164+
""".trimIndent()
165+
)
166+
val result = testProject.build(
167+
"-Duser.home=${testingHomeFolder}",
168+
"-Pvaadin.productionMode",
169+
"vaadinBuildFrontend"
170+
)
171+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
172+
assertContains(result.output, "Application watermark enabled")
173+
174+
assertTrue { buildInfo.exists() }
175+
val buildInfoJson = buildInfo.readText()
176+
assertContains(
177+
buildInfoJson,
178+
Regex("(?s).*\"watermark\\.enable\"\\s*:\\s*true.*"),
179+
"watermark.enable token missing or incorrect in ${buildInfo.absolutePath}: ${buildInfoJson}"
180+
)
181+
}
182+
183+
@Test
184+
fun testBuildFrontendInProductionMode_watermarkBuildEnabledByGradleProperty_buildSucceeds() {
185+
val result = testProject.build(
186+
"-Duser.home=${testingHomeFolder}",
187+
"-Pvaadin.commercialWithWatermark",
188+
"-Pvaadin.productionMode",
189+
"vaadinBuildFrontend"
190+
)
191+
result.expectTaskOutcome("vaadinBuildFrontend", TaskOutcome.SUCCESS)
192+
assertContains(result.output, "Application watermark enabled")
193+
194+
assertTrue { buildInfo.exists() }
195+
val buildInfoJson = buildInfo.readText()
196+
assertContains(
197+
buildInfoJson,
198+
Regex("(?s).*\"watermark\\.enable\"\\s*:\\s*true.*"),
199+
"watermark.enable token missing or incorrect in ${buildInfo.absolutePath}: ${buildInfoJson}"
200+
)
201+
202+
}
203+
204+
companion object {
205+
206+
@ClassRule
207+
@JvmField
208+
val tempHomeFolder = TemporaryFolder()
209+
lateinit var realUserHome: String
210+
lateinit var testingHomeFolder: String
211+
212+
@BeforeClass
213+
@JvmStatic
214+
fun createFakeHome() {
215+
realUserHome = System.getProperty("user.home");
216+
val userHomeFolder = File(realUserHome)
217+
val vaadinHomeNodeFolder =
218+
userHomeFolder.resolve(".vaadin").resolve("node")
219+
// Try to speed up test by copying existing node into the fake home
220+
if (vaadinHomeNodeFolder.isDirectory) {
221+
val fakeVaadinHomeNode =
222+
tempHomeFolder.root.resolve(".vaadin").resolve("node")
223+
fakeVaadinHomeNode.mkdirs();
224+
vaadinHomeNodeFolder.copyRecursively(fakeVaadinHomeNode);
225+
// copyRecursively does not preserve file attributes
226+
// fix it by manually setting executable flag so that node can
227+
// be launched
228+
vaadinHomeNodeFolder.walkTopDown().filter {
229+
it.canExecute()
230+
}.forEach {
231+
val relative = it.relativeTo(vaadinHomeNodeFolder)
232+
val destination = fakeVaadinHomeNode.resolve(relative.path)
233+
destination.setExecutable(true)
234+
}
235+
}
236+
testingHomeFolder = tempHomeFolder.root.absolutePath
237+
System.setProperty("user.home", testingHomeFolder)
238+
}
239+
240+
@AfterClass
241+
@JvmStatic
242+
fun restoreUserHomeSystemProperty() {
243+
System.setProperty("user.home", realUserHome)
244+
}
245+
246+
}
247+
248+
}

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ internal class GradlePluginAdapter private constructor(
123123
it.componentFilter { componentId ->
124124
// a componentId different ModuleComponentIdentifier
125125
// could be a local library, should not be filtered out
126-
val accepted = componentId !is ModuleComponentIdentifier || artifactFilter.test(
127-
componentId.moduleIdentifier
128-
)
126+
val accepted =
127+
componentId !is ModuleComponentIdentifier || artifactFilter.test(
128+
componentId.moduleIdentifier
129+
)
129130
accepted
130131
}
131132
}.files
@@ -314,6 +315,11 @@ internal class GradlePluginAdapter private constructor(
314315
override fun frontendExtraFileExtensions(): List<String> =
315316
config.frontendExtraFileExtensions.get()
316317

317-
override fun isFrontendIgnoreVersionChecks(): Boolean = config.frontendIgnoreVersionChecks.get()
318+
override fun isFrontendIgnoreVersionChecks(): Boolean =
319+
config.frontendIgnoreVersionChecks.get()
320+
321+
override fun isWatermarkEnabled(): Boolean {
322+
return config.commercialWithWatermark.get()
323+
}
318324

319325
}

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinBuildFrontendTask.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ package com.vaadin.flow.gradle
1818
import com.vaadin.experimental.FeatureFlags
1919
import com.vaadin.flow.plugin.base.BuildFrontendUtil
2020
import com.vaadin.flow.server.Constants
21+
import com.vaadin.flow.server.InitParameters
2122
import com.vaadin.flow.server.frontend.BundleValidationUtil
2223
import com.vaadin.flow.server.frontend.FrontendUtils
2324
import com.vaadin.flow.server.frontend.Options
2425
import com.vaadin.flow.server.frontend.TaskCleanFrontendFiles
25-
import com.vaadin.flow.server.frontend.scanner.ClassFinder
2626
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner
2727
import com.vaadin.flow.server.frontend.scanner.FrontendDependenciesScanner.FrontendDependenciesScannerFactory
2828
import com.vaadin.pro.licensechecker.LicenseChecker
29+
import com.vaadin.pro.licensechecker.MissingLicenseKeyException
2930
import org.gradle.api.DefaultTask
3031
import org.gradle.api.provider.Property
3132
import org.gradle.api.tasks.Internal
@@ -116,9 +117,20 @@ public abstract class VaadinBuildFrontendTask : DefaultTask() {
116117
}
117118
}
118119
LicenseChecker.setStrictOffline(true)
119-
val licenseRequired = BuildFrontendUtil.validateLicenses(adapter.get(), frontendDependencies)
120+
val (licenseRequired: Boolean, watermarkRequired: Boolean) = try {
121+
Pair(
122+
BuildFrontendUtil.validateLicenses(
123+
adapter.get(),
124+
frontendDependencies
125+
), false
126+
)
127+
} catch (e: MissingLicenseKeyException) {
128+
logger.info(e.message)
129+
Pair(true, true)
130+
}
120131

121-
BuildFrontendUtil.updateBuildFile(adapter.get(), licenseRequired)
132+
BuildFrontendUtil.updateBuildFile(adapter.get(), licenseRequired, watermarkRequired
133+
)
122134
}
123135

124136

flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,12 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val
313313
*/
314314
public abstract val frontendIgnoreVersionChecks: Property<Boolean>
315315

316+
/**
317+
* Allows building a watermarked version of the application when commercial
318+
* components are used without a license key.
319+
*/
320+
public abstract val commercialWithWatermark: Property<Boolean>
321+
316322
public fun filterClasspath(
317323
@DelegatesTo(
318324
value = ClasspathFilter::class,
@@ -593,6 +599,16 @@ public class PluginEffectiveConfiguration(
593599
public val npmExcludeWebComponents: Provider<Boolean> = extension
594600
.npmExcludeWebComponents.convention(false)
595601

602+
public val commercialWithWatermark: Provider<Boolean> =
603+
extension.commercialWithWatermark.convention(false)
604+
.overrideWithSystemPropertyFlag(
605+
project, InitParameters.COMMERCIAL_WITH_WATERMARK
606+
)
607+
.overrideWithSystemPropertyFlag(
608+
project,
609+
"vaadin.${InitParameters.COMMERCIAL_WITH_WATERMARK}"
610+
)
611+
596612
public val toolsSettings: Provider<FrontendToolsSettings> = npmFolder.map {
597613
FrontendToolsSettings(it.absolutePath) {
598614
FrontendUtils.getVaadinHomeDirectory()
@@ -668,6 +684,7 @@ public class PluginEffectiveConfiguration(
668684
"cleanFrontendFiles=${cleanFrontendFiles.get()}," +
669685
"frontendExtraFileExtensions=${frontendExtraFileExtensions.get()}," +
670686
"npmExcludeWebComponents=${npmExcludeWebComponents.get()}" +
687+
"commercialWithWatermark=${commercialWithWatermark.get()}" +
671688
")"
672689

673690
public companion object {

flow-plugins/flow-maven-plugin/src/it/flow-addon/invoker.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ invoker.profiles.2=fake-flow-resources
2323
invoker.profiles.3=fake-flow-plugin-resources
2424
invoker.profiles.4=alpha-addon
2525
invoker.profiles.5=beta-addon
26+
invoker.profiles.6=commercial-addon

flow-plugins/flow-maven-plugin/src/it/flow-addon/pom.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,36 @@
110110
</resources>
111111
</build>
112112
</profile>
113+
<profile>
114+
<id>commercial-addon</id>
115+
<properties>
116+
<custom.source.directory>src/main/commercial-addon/java</custom.source.directory>
117+
</properties>
118+
<build>
119+
<finalName>commercial-addon-${project.version}</finalName>
120+
<resources>
121+
<resource>
122+
<directory>${project.basedir}/src/main/commercial-addon/resources</directory>
123+
</resource>
124+
</resources>
125+
<plugins>
126+
<plugin>
127+
<groupId>org.apache.maven.plugins</groupId>
128+
<artifactId>maven-jar-plugin</artifactId>
129+
<configuration>
130+
<archive>
131+
<manifest>
132+
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
133+
</manifest>
134+
<manifestEntries>
135+
<CvdlName>vaadin-commercial-component</CvdlName>
136+
</manifestEntries>
137+
</archive>
138+
</configuration>
139+
</plugin>
140+
</plugins>
141+
</build>
142+
</profile>
113143
</profiles>
114144

115145
</project>

0 commit comments

Comments
 (0)