![JAR search and dependency download from the Maven repository](/logo.png)
org.akhikhl.gretty.GrettyPlugin.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gretty Show documentation
Show all versions of gretty Show documentation
Advanced gradle plugin for running web-apps on jetty and tomcat
/*
* Gretty
*
* Copyright (C) 2013-2015 Andrey Hihlovskiy and contributors.
*
* See the file "LICENSE" for copying and usage permission.
* See the file "CONTRIBUTORS" for complete list of contributors.
*/
package org.akhikhl.gretty
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import groovy.util.XmlSlurper
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ResolvedArtifact
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
*
* @author akhikhl
*/
class GrettyPlugin implements Plugin {
protected static final Logger log = LoggerFactory.getLogger(GrettyPlugin)
private void addConfigurations(Project project) {
project.configurations {
compile {
exclude module: 'spring-boot-starter-tomcat'
exclude module: 'spring-boot-starter-jetty'
}
gretty
grettyStarter
springBoot {
exclude module: 'spring-boot-starter-tomcat'
exclude module: 'spring-boot-starter-jetty'
exclude group: 'org.eclipse.jetty'
exclude group: 'org.eclipse.jetty.websocket'
}
grettyNoSpringBoot {
extendsFrom project.configurations.gretty
exclude group: 'org.springframework.boot'
}
grettySpringLoaded {
transitive = false
}
grettyProductRuntime
grettyProvidedCompile
project.configurations.findByName('compile')?.extendsFrom grettyProvidedCompile
}
ServletContainerConfig.getConfigs().each { configName, config ->
project.configurations.create config.servletContainerRunnerConfig
}
}
private void addConfigurationsAfterEvaluate(Project project) {
def runtimeConfig = project.configurations.findByName('runtime')
project.configurations {
springBoot {
if (runtimeConfig)
extendsFrom runtimeConfig
}
grettyProductRuntime {
if (runtimeConfig)
extendsFrom runtimeConfig
}
}
// need to configure providedCompile, so that war excludes grettyProvidedCompile artifacts
def providedCompile = project.configurations.findByName('providedCompile')
if(providedCompile)
providedCompile.extendsFrom project.configurations.grettyProvidedCompile
SpringBootResolutionStrategy.apply(project)
}
private void addDependencies(Project project) {
String grettyVersion = Externalized.getString('grettyVersion')
String springBootVersion = project.gretty.springBootVersion ?: (project.hasProperty('springBootVersion') ? project.springBootVersion : Externalized.getString('springBootVersion'))
String springLoadedVersion = project.gretty.springLoadedVersion ?: (project.hasProperty('springLoadedVersion') ? project.springLoadedVersion : Externalized.getString('springLoadedVersion'))
String springVersion = project.gretty.springVersion ?: (project.hasProperty('springVersion') ? project.springVersion : Externalized.getString('springVersion'))
String slf4jVersion = Externalized.getString('slf4jVersion')
String logbackVersion = Externalized.getString('logbackVersion')
project.dependencies {
grettyStarter "org.akhikhl.gretty:gretty-starter:$grettyVersion"
grettySpringLoaded "org.springframework:springloaded:$springLoadedVersion"
}
ServletContainerConfig.getConfig(project.gretty.servletContainer).with { config ->
def closure = config.servletApiDependencies
closure = closure.rehydrate(config, closure.owner, closure.thisObject)
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure(project)
}
ServletContainerConfig.getConfigs().each { configName, config ->
def closure = config.servletContainerRunnerDependencies
closure = closure.rehydrate(config, closure.owner, closure.thisObject)
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure(project)
}
if(project.gretty.springBoot) {
String configName = project.configurations.findByName('compile') ? 'compile' : 'springBoot'
project.dependencies.add configName, "org.springframework.boot:spring-boot-starter-web:$springBootVersion", {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
project.dependencies.add configName, "org.springframework.boot:spring-boot-starter-websocket:${springBootVersion}", {
exclude group: 'org.apache.tomcat.embed'
}
project.dependencies.add configName, "org.springframework:spring-messaging:$springVersion"
project.dependencies.add configName, "org.springframework:spring-websocket:$springVersion"
project.dependencies.add configName, "ch.qos.logback:logback-classic:$logbackVersion"
configName = project.configurations.findByName('runtime') ? 'runtime' : 'springBoot'
project.dependencies.add configName, "org.akhikhl.gretty:gretty-springboot:$grettyVersion"
}
for(String overlay in project.gretty.overlays)
project.dependencies.add 'grettyProvidedCompile', project.project(overlay)
ProjectUtils.withOverlays(project).find { proj ->
boolean alteredDependencies = false
File webXmlFile = new File(ProjectUtils.getWebAppDir(proj), 'WEB-INF/web.xml')
if(webXmlFile.exists()) {
def webXml = new XmlSlurper().parse(webXmlFile)
if(webXml.filter.find { it.'filter-class'.text() == 'org.akhikhl.gretty.RedirectFilter' }) {
project.dependencies {
compile "org.akhikhl.gretty:gretty-filter:${project.ext.grettyVersion}", {
exclude group: 'javax.servlet', module: 'servlet-api'
}
}
alteredDependencies = true
}
}
alteredDependencies
}
def runtimeConfig = project.configurations.findByName('runtime')
if(runtimeConfig) {
def artifacts = runtimeConfig.copyRecursive().resolvedConfiguration.resolvedArtifacts
if(artifacts.find { it.name == 'slf4j-api' } && !artifacts.find { it.name in ['slf4j-nop', 'slf4j-simple', 'slf4j-log4j12', 'slf4j-jdk14', 'logback-classic'] }) {
project.dependencies {
compile "org.slf4j:slf4j-nop:$slf4jVersion"
}
}
}
}
private void addExtensions(Project project) {
project.extensions.create('gretty', GrettyExtension)
project.extensions.create('farm', FarmExtension, project)
project.extensions.create('farms', FarmsExtension, project)
project.farms.farmsMap_[''] = project.farm
project.extensions.create('product', ProductExtension)
project.extensions.create('products', ProductsExtension)
project.products.productsMap[''] = project.product
}
private void addRepositories(Project project) {
project.repositories {
mavenLocal()
jcenter()
mavenCentral()
maven { url 'http://repo.spring.io/release' }
maven { url 'http://repo.spring.io/milestone' }
maven { url 'http://repo.spring.io/snapshot' }
}
}
private void addTaskDependencies(Project project) {
project.tasks.whenObjectAdded { task ->
if(GradleUtils.instanceOf(task, 'org.akhikhl.gretty.AppStartTask'))
task.dependsOn {
// We don't need any task for hard inplace mode.
task.effectiveInplace ? project.tasks.prepareInplaceWebApp : project.tasks.prepareArchiveWebApp
}
else if(GradleUtils.instanceOf(task, 'org.akhikhl.gretty.FarmStartTask'))
task.dependsOn {
task.getWebAppConfigsForProjects().findResults {
def proj = project.project(it.projectPath)
boolean inplace = it.inplace == null ? task.inplace : it.inplace
String prepareTaskName = inplace ? 'prepareInplaceWebApp' : 'prepareArchiveWebApp'
def projTask = proj.tasks.findByName(prepareTaskName)
if(!projTask)
proj.tasks.whenObjectAdded { t ->
if(t.name == prepareTaskName)
task.dependsOn t
}
projTask
}
}
}
}
private void addTasks(Project project) {
if(project.tasks.findByName('classes')) { // JVM project?
project.task('prepareInplaceWebAppFolder', group: 'gretty') {
description = 'Copies webAppDir of this web-app and all overlays (if any) to ${buildDir}/inplaceWebapp'
def getInplaceMode = {
project.tasks.findByName('appRun').effectiveInplaceMode
}
inputs.dir ProjectUtils.getWebAppDir(project)
// We should track changes in inplaceMode value or plugin would show UP-TO-DATE for this task
// even if inplaceMode was changed
inputs.property('inplaceMode', getInplaceMode)
outputs.dir "${project.buildDir}/inplaceWebapp"
doLast {
if(getInplaceMode() != 'hard') {
// Skipping this task for hard inplaceMode.
ProjectUtils.prepareInplaceWebAppFolder(project)
}
}
}
project.task('prepareInplaceWebAppClasses', group: 'gretty') {
description = 'Compiles classes of this web-app and all overlays (if any)'
dependsOn project.tasks.classes
for(String overlay in project.gretty.overlays)
dependsOn "$overlay:prepareInplaceWebAppClasses"
}
project.task('prepareInplaceWebApp', group: 'gretty') {
description = 'Prepares inplace web-app'
dependsOn project.tasks.prepareInplaceWebAppFolder
dependsOn project.tasks.prepareInplaceWebAppClasses
}
def archiveTask = project.tasks.findByName('war') ?: project.tasks.jar
archiveTask.configure project.gretty.webappCopy
if(project.gretty.overlays) {
project.ext.finalArchivePath = archiveTask.archivePath
archiveTask.archiveName = 'partial.' + (project.tasks.findByName('war') ? 'war' : 'jar')
// 'explodeWebApps' task is only activated by 'overlayArchive' task
project.task('explodeWebApps', group: 'gretty') {
description = 'Explodes this web-app and all overlays (if any) to ${buildDir}/explodedWebapp'
for(String overlay in project.gretty.overlays)
dependsOn "$overlay:assemble" as String
dependsOn archiveTask
for(String overlay in project.gretty.overlays)
inputs.file { ProjectUtils.getFinalArchivePath(project.project(overlay)) }
inputs.file archiveTask.archivePath
outputs.dir "${project.buildDir}/explodedWebapp"
doLast {
ProjectUtils.prepareExplodedWebAppFolder(project)
}
}
project.task('overlayArchive', group: 'gretty') {
description = 'Creates archive from exploded web-app in ${buildDir}/explodedWebapp'
dependsOn project.tasks.explodeWebApps
inputs.dir "${project.buildDir}/explodedWebapp"
outputs.file project.ext.finalArchivePath
doLast {
ant.zip destfile: project.ext.finalArchivePath, basedir: "${project.buildDir}/explodedWebapp"
}
}
project.tasks.assemble.dependsOn project.tasks.overlayArchive
} // overlays
project.task('prepareArchiveWebApp', group: 'gretty') {
description = 'Prepares war web-app'
if(project.gretty.overlays)
dependsOn project.tasks.overlayArchive
else
dependsOn archiveTask
}
project.task('appRun', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in interactive mode.'
}
project.tasks.run.dependsOn 'appRun'
project.task('appRunDebug', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug and interactive mode.'
debug = true
}
project.tasks.debug.dependsOn 'appRunDebug'
project.task('appStart', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app inplace (stopped by \'appStop\').'
interactive = false
}
project.task('appStartDebug', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug mode (stopped by \'appStop\').'
interactive = false
debug = true
}
if(project.plugins.findPlugin(org.gradle.api.plugins.WarPlugin)) {
project.task('appRunWar', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in interactive mode.'
inplace = false
}
project.task('appRunWarDebug', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug and interactive mode.'
inplace = false
debug = true
}
project.task('appStartWar', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file (stopped by \'appStop\').'
inplace = false
interactive = false
}
project.task('appStartWarDebug', type: AppStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug mode (stopped by \'appStop\').'
inplace = false
interactive = false
debug = true
}
}
project.task('appStop', type: AppStopTask, group: 'gretty') {
description = 'Sends \'stop\' command to a running server.'
}
project.task('appRestart', type: AppRestartTask, group: 'gretty') {
description = 'Sends \'restart\' command to a running server.'
}
project.task('appBeforeIntegrationTest', type: AppBeforeIntegrationTestTask, group: 'gretty') {
description = 'Starts server before integration test.'
}
project.task('appAfterIntegrationTest', type: AppAfterIntegrationTestTask, group: 'gretty') {
description = 'Stops server after integration test.'
}
project.task('jettyRun', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in interactive mode.'
}
project.task('jettyRunDebug', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug and interactive mode.'
debug = true
}
project.task('jettyStart', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app inplace (stopped by \'jettyStop\').'
interactive = false
}
project.task('jettyStartDebug', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug mode (stopped by \'jettyStop\').'
interactive = false
debug = true
}
if(project.plugins.findPlugin(org.gradle.api.plugins.WarPlugin)) {
project.task('jettyRunWar', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in interactive mode.'
inplace = false
}
project.task('jettyRunWarDebug', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug and interactive mode.'
inplace = false
debug = true
}
project.task('jettyStartWar', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file (stopped by \'jettyStop\').'
inplace = false
interactive = false
}
project.task('jettyStartWarDebug', type: JettyStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug mode (stopped by \'jettyStop\').'
inplace = false
interactive = false
debug = true
}
}
project.task('jettyStop', type: AppStopTask, group: 'gretty') {
description = 'Sends \'stop\' command to a running server.'
}
project.task('jettyRestart', type: AppRestartTask, group: 'gretty') {
description = 'Sends \'restart\' command to a running server.'
}
project.task('tomcatRun', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in interactive mode.'
}
project.task('tomcatRunDebug', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug and interactive mode.'
debug = true
}
project.task('tomcatStart', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app inplace (stopped by \'tomcatStop\').'
interactive = false
}
project.task('tomcatStartDebug', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app inplace, in debug mode (stopped by \'tomcatStop\').'
interactive = false
debug = true
}
if(project.plugins.findPlugin(org.gradle.api.plugins.WarPlugin)) {
project.task('tomcatRunWar', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in interactive mode.'
inplace = false
}
project.task('tomcatRunWarDebug', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug and interactive mode.'
inplace = false
debug = true
}
project.task('tomcatStartWar', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file (stopped by \'tomcatStop\').'
inplace = false
interactive = false
}
project.task('tomcatStartWarDebug', type: TomcatStartTask, group: 'gretty') {
description = 'Starts web-app on WAR-file, in debug mode (stopped by \'tomcatStop\').'
inplace = false
interactive = false
debug = true
}
}
project.task('tomcatStop', type: AppStopTask, group: 'gretty') {
description = 'Sends \'stop\' command to a running server.'
}
project.task('tomcatRestart', type: AppRestartTask, group: 'gretty') {
description = 'Sends \'restart\' command to a running server.'
}
project.ext.jettyBeforeIntegrationTest = project.tasks.ext.jettyBeforeIntegrationTest = project.tasks.appBeforeIntegrationTest
project.ext.jettyAfterIntegrationTest = project.tasks.ext.jettyAfterIntegrationTest = project.tasks.appAfterIntegrationTest
project.task('showClassPath', group: 'gretty') {
description = 'Shows classpath information'
doLast {
println "Runner classpath:"
project.tasks.appRun.runnerClassPath.each { URL url ->
println " $url"
}
project.tasks.appRun.getWebappClassPaths().each { contextPath, cp ->
println "$contextPath classpath:"
cp.each { URL url ->
println " $url"
}
}
}
}
} // JVM project
project.farms.farmsMap.each { fname, farm ->
String farmDescr = fname ? "farm '${fname}'" : 'default farm'
project.task('farmRun' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} inplace, in interactive mode."
farmName = fname
if(!fname)
doFirst {
GradleUtils.disableTaskOnOtherProjects(project, 'run')
GradleUtils.disableTaskOnOtherProjects(project, 'jettyRun')
GradleUtils.disableTaskOnOtherProjects(project, 'farmRun')
}
}
project.task('farmRunDebug' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} inplace, in debug and in interactive mode."
farmName = fname
debug = true
if(!fname)
doFirst {
GradleUtils.disableTaskOnOtherProjects(project, 'debug')
GradleUtils.disableTaskOnOtherProjects(project, 'jettyRunDebug')
GradleUtils.disableTaskOnOtherProjects(project, 'farmRunDebug')
}
}
project.task('farmStart' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} inplace (stopped by 'farmStop${fname}')."
farmName = fname
interactive = false
}
project.task('farmStartDebug' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} inplace, in debug mode (stopped by 'farmStop${fname}')."
farmName = fname
interactive = false
debug = true
}
project.task('farmRunWar' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} on WAR-files, in interactive mode."
farmName = fname
inplace = false
}
project.task('farmRunWarDebug' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} on WAR-files, in debug and in interactive mode."
farmName = fname
debug = true
inplace = false
}
project.task('farmStartWar' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} on WAR-files (stopped by 'farmStop${fname}')."
farmName = fname
interactive = false
inplace = false
}
project.task('farmStartWarDebug' + fname, type: FarmStartTask, group: 'gretty') {
description = "Starts ${farmDescr} on WAR-files, in debug (stopped by 'farmStop${fname}')."
farmName = fname
interactive = false
debug = true
inplace = false
}
project.task('farmStop' + fname, type: FarmStopTask, group: 'gretty') {
description = "Sends \'stop\' command to a running ${farmDescr}."
farmName = fname
}
project.task('farmRestart' + fname, type: FarmRestartTask, group: 'gretty') {
description = "Sends \'restart\' command to a running ${farmDescr}."
farmName = fname
}
project.task('farmBeforeIntegrationTest' + fname, type: FarmBeforeIntegrationTestTask, group: 'gretty') {
description = "Starts ${farmDescr} before integration test."
farmName = fname
}
project.task('farmIntegrationTest' + fname, type: FarmIntegrationTestTask, group: 'gretty') {
description = "Runs integration tests on ${farmDescr} web-apps."
farmName = fname
dependsOn 'farmBeforeIntegrationTest' + fname
finalizedBy 'farmAfterIntegrationTest' + fname
}
project.task('farmAfterIntegrationTest' + fname, type: FarmAfterIntegrationTestTask, group: 'gretty') {
description = "Stops ${farmDescr} after integration test."
farmName = fname
}
} // farmsMap
} // addTasks
private void afterProjectEvaluate(Project project) {
if(project.extensions.findByName('gretty')) {
addConfigurationsAfterEvaluate(project)
addTaskDependencies(project)
new ProductsConfigurer(project).configureProducts()
if(project.gretty.autoConfigureRepositories)
addRepositories(project)
addDependencies(project)
addTasks(project)
for(Closure afterEvaluateClosure in project.gretty.afterEvaluate) {
afterEvaluateClosure.delegate = project.gretty
afterEvaluateClosure.resolveStrategy = Closure.DELEGATE_FIRST
afterEvaluateClosure()
}
project.tasks.findByName('appBeforeIntegrationTest')?.with {
if(!integrationTestTaskAssigned)
integrationTestTask null // default binding
}
project.tasks.findByName('appAfterIntegrationTest')?.with {
if(!integrationTestTaskAssigned)
integrationTestTask null // default binding
}
}
}
private void afterAllProjectsEvaluate(Project rootProject) {
rootProject.allprojects { project ->
if(project.extensions.findByName('farms'))
project.farms.farmsMap.each { fname, farm ->
for(Closure afterEvaluateClosure in farm.afterEvaluate) {
afterEvaluateClosure.delegate = farm
afterEvaluateClosure.resolveStrategy = Closure.DELEGATE_FIRST
afterEvaluateClosure()
}
if(!project.tasks."farmBeforeIntegrationTest$fname".integrationTestTaskAssigned)
project.tasks."farmBeforeIntegrationTest$fname".integrationTestTask null // default binding
if(!project.tasks."farmIntegrationTest$fname".integrationTestTaskAssigned)
project.tasks."farmIntegrationTest$fname".integrationTestTask null // default binding
if(!project.tasks."farmAfterIntegrationTest$fname".integrationTestTaskAssigned)
project.tasks."farmAfterIntegrationTest$fname".integrationTestTask null // default binding
}
}
}
void apply(final Project project) {
if(project.gradle.gradleVersion.startsWith('1.')) {
String releaseNumberStr = project.gradle.gradleVersion.split('\\.')[1]
if(releaseNumberStr.contains('-'))
releaseNumberStr = releaseNumberStr.split('-')[0]
int releaseNumber = releaseNumberStr as int
if(releaseNumber < 10)
throw new GradleException("Gretty supports only Gradle 1.10 or newer. You have Gradle ${project.gradle.gradleVersion}.")
}
project.ext {
grettyVersion = Externalized.getString('grettyVersion')
jetty7Version = Externalized.getString('jetty7Version')
jetty7ServletApi = Externalized.getString('jetty7ServletApi')
jetty7ServletApiVersion = Externalized.getString('jetty7ServletApiVersion')
jetty8Version = Externalized.getString('jetty8Version')
jetty8ServletApi = Externalized.getString('jetty8ServletApi')
jetty8ServletApiVersion = Externalized.getString('jetty8ServletApiVersion')
jetty9Version = Externalized.getString('jetty9Version')
jetty9ServletApi = Externalized.getString('jetty9ServletApi')
jetty9ServletApiVersion = Externalized.getString('jetty9ServletApiVersion')
tomcat7Version = Externalized.getString('tomcat7Version')
tomcat7ServletApi = Externalized.getString('tomcat7ServletApi')
tomcat7ServletApiVersion = Externalized.getString('tomcat7ServletApiVersion')
tomcat8Version = Externalized.getString('tomcat8Version')
tomcat8ServletApi = Externalized.getString('tomcat8ServletApi')
tomcat8ServletApiVersion = Externalized.getString('tomcat8ServletApiVersion')
}
if(!project.tasks.findByName('run'))
project.task('run', group: 'gretty') {
description = 'Starts web-app inplace, in interactive mode. Same as appRun task.'
}
if(!project.tasks.findByName('debug'))
project.task('debug', group: 'gretty') {
description = 'Starts web-app inplace, in debug and interactive mode. Same as appRunDebug task.'
}
addExtensions(project)
addConfigurations(project)
if(!project.rootProject.hasProperty('gretty_')) {
Project rootProject = project.rootProject
rootProject.ext.gretty_ = [:]
rootProject.ext.gretty_.projects = []
rootProject.allprojects { proj ->
afterEvaluate {
afterProjectEvaluate(proj)
rootProject.ext.gretty_.projects.add proj
if(rootProject.ext.gretty_.projects.size() == rootProject.allprojects.size())
afterAllProjectsEvaluate(rootProject)
}
}
}
} // apply
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy