Merge pull request #104 from kelemen/generated-jakarta-sources
FREEMARKER-218: Jakarta support
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ef7730d..0f7ae19 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,6 +48,11 @@
with:
java-version: 16
distribution: zulu
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: oracle
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1.1.0
- name: Run Build
@@ -60,4 +65,3 @@
name: test-reports-${{ matrix.os }}
path: build/reports/**
retention-days: 30
-
diff --git a/README.md b/README.md
index e282628..384c486 100644
--- a/README.md
+++ b/README.md
@@ -106,7 +106,7 @@
the source code repository. See repository locations here:
https://freemarker.apache.org/sourcecode.html
-You need JDK 8 and JDK 16 to be installed
+You need JDK 8, JDK 16 and JDK 17 (only for some tests) to be installed
(and [visible to Gradle](https://docs.gradle.org/current/userguide/toolchains.html)).
Be sure that your default Java version (which Gradle should use automatically) is at
diff --git a/build.gradle.kts b/build.gradle.kts
index 6e6f1f9..84f2c3b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,7 +20,7 @@
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets
import java.nio.file.Files
-import java.util.*
+import java.util.Properties
import java.util.stream.Collectors
plugins {
@@ -58,6 +58,36 @@
configureSourceSet("jython22")
configureSourceSet("jython25") { enableTests() }
configureSourceSet("core16", "16")
+
+ configureGeneratedSourceSet("jakartaServlet") {
+ val jakartaSourceGenerators = generateJakartaSources("javaxServlet")
+
+ val testSourceSet = enableTests("17").get().sources
+ val jakartaTestSourceGenerators = generateJakartaSources(
+ "javaxServletTest",
+ SourceSet.TEST_SOURCE_SET_NAME,
+ testSourceSet
+ )
+
+ (jakartaSourceGenerators + jakartaTestSourceGenerators).forEach { task ->
+ task.configure {
+ packageMappings.set(mapOf(
+ "freemarker.ext.jsp" to "freemarker.ext.jakarta.jsp",
+ "freemarker.ext.servlet" to "freemarker.ext.jakarta.servlet",
+ "freemarker.cache" to "freemarker.ext.jakarta.servlet",
+ ))
+ noAutoReplacePackages.set(setOf("freemarker.cache"))
+ replacements.set(mapOf(
+ "package freemarker.cache" to "package freemarker.ext.jakarta.servlet",
+ "freemarker.cache.WebappTemplateLoader" to "freemarker.ext.jakarta.servlet.WebappTemplateLoader",
+ "javax.servlet" to "jakarta.servlet",
+ "javax.el" to "jakarta.el",
+ "http://java.sun.com/jsp/jstl/core" to "jakarta.tags.core",
+ "http://java.sun.com/jsp/jstl/functions" to "jakarta.tags.functions",
+ ))
+ }
+ }
+ }
}
val compileJavacc = tasks.register<freemarker.build.CompileJavaccTask>("compileJavacc") {
@@ -127,6 +157,10 @@
extendsFrom(named("jython25CompileClasspath").get())
extendsFrom(named("javaxServletCompileClasspath").get())
}
+ register("javadocClasspath") {
+ extendsFrom(named("combinedClasspath").get())
+ extendsFrom(named("jakartaServletCompileClasspath").get())
+ }
}
// This source set is only needed, because the OSGI plugin supports only a single sourceSet.
@@ -230,7 +264,7 @@
addStringOption("Xdoclint:-missing", "-quiet")
}
- classpath = files(configurations.named("combinedClasspath"))
+ classpath = files(configurations.named("javadocClasspath"))
}
fun registerManualTask(taskName: String, localeValue: String, offlineValue: Boolean) {
@@ -441,14 +475,14 @@
val props = Properties().apply {
// see https://reproducible-builds.org/docs/jvm/
setProperty("buildinfo.version", "1.0-SNAPSHOT")
-
+
setProperty("java.version", System.getProperty("java.version"))
setProperty("java.vendor", System.getProperty("java.vendor"))
setProperty("os.name", System.getProperty("os.name"))
-
+
setProperty("source.scm.uri", "scm:git:https://git-wip-us.apache.org/repos/asf/freemarker.git")
setProperty("source.scm.tag", "v${fmExt.versionDef.version}")
-
+
setProperty("build-tool", "gradle")
setProperty("build.setup", "https://github.com/apache/freemarker/blob/2.3-gae/README.md#building-freemarker")
@@ -560,9 +594,13 @@
val jettyVersion = "9.4.53.v20231009"
val slf4jVersion = "1.6.1"
-val springVersion = "2.5.6.SEC03"
+val springVersion = "5.3.31"
val tagLibsVersion = "1.2.5"
+val jakartaJettyVersion = "11.0.19"
+val jakartaSlf4jVersion = "2.0.9"
+val jakartaSpringVersion = "6.1.2"
+
configurations {
compileOnly {
exclude(group = "xml-apis", module = "xml-apis")
@@ -599,6 +637,10 @@
testImplementation(xalan)
+ "jakartaServletCompileOnly"("jakarta.servlet:jakarta.servlet-api:5.0.0")
+ "jakartaServletCompileOnly"("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0")
+ "jakartaServletCompileOnly"("jakarta.el:jakarta.el-api:4.0.0")
+
"javaxServletCompileOnly"("javax.servlet:javax.servlet-api:3.0.1")
"javaxServletCompileOnly"("javax.servlet.jsp:jsp-api:2.2")
"javaxServletCompileOnly"("javax.el:el-api:2.2")
@@ -619,6 +661,39 @@
"javaxServletTestImplementation"("org.springframework:spring-test:${springVersion}") {
exclude(group = "commons-logging", module = "commons-logging")
}
+ "javaxServletTestImplementation"("org.springframework:spring-web:${springVersion}") {
+ exclude(group = "commons-logging", module = "commons-logging")
+ }
+ "javaxServletTestImplementation"("com.github.hazendaz:displaytag:2.5.3")
+
+ "jakartaServletTestImplementation"("org.eclipse.jetty:jetty-server:${jakartaJettyVersion}")
+ "jakartaServletTestImplementation"("org.eclipse.jetty:jetty-annotations:${jakartaJettyVersion}")
+ "jakartaServletTestImplementation"("org.eclipse.jetty:jetty-webapp:${jakartaJettyVersion}")
+ "jakartaServletTestImplementation"("org.eclipse.jetty:jetty-util:${jakartaJettyVersion}")
+ "jakartaServletTestImplementation"("org.eclipse.jetty:apache-jsp:${jakartaJettyVersion}")
+ // Jetty also contains the servlet-api and jsp-api and el-api classes
+
+ "jakartaServletTestImplementation"("jakarta.servlet:jakarta.servlet-api:6.0.0")
+ "jakartaServletTestImplementation"("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0")
+ "jakartaServletTestImplementation"("jakarta.el:jakarta.el-api:4.0.0")
+
+ // JSP JSTL (not included in Jetty):
+ "jakartaServletTestImplementation"("com.github.hazendaz:displaytag:3.0.0-M2")
+
+ "jakartaServletTestImplementation"("org.springframework:spring-core:${jakartaSpringVersion}") {
+ exclude(group = "commons-logging", module = "commons-logging")
+ }
+ "jakartaServletTestImplementation"("org.springframework:spring-test:${jakartaSpringVersion}") {
+ exclude(group = "commons-logging", module = "commons-logging")
+ }
+ "jakartaServletTestImplementation"("org.springframework:spring-web:${jakartaSpringVersion}") {
+ exclude(group = "commons-logging", module = "commons-logging")
+ }
+
+ "jakartaServletTestRuntimeOnly"("org.slf4j:slf4j-api:${jakartaSlf4jVersion}")
+ "jakartaServletTestRuntimeOnly"("org.slf4j:log4j-over-slf4j:${jakartaSlf4jVersion}")
+ "jakartaServletTestRuntimeOnly"("org.slf4j:jcl-over-slf4j:${jakartaSlf4jVersion}")
+ "jakartaServletTestRuntimeOnly"("ch.qos.logback:logback-classic:1.3.14")
"jython20CompileOnly"("jython:jython:2.1")
@@ -628,13 +703,6 @@
"jython25CompileOnly"(sourceSets["jython20"].output)
"jython25CompileOnly"("org.python:jython:2.5.0")
- "testUtilsImplementation"("com.github.hazendaz:displaytag:2.5.3") {
- exclude(group = "com.lowagie", module = "itext")
- // We manage logging centrally:
- exclude(group = "org.slf4j", module = "slf4j-log4j12")
- exclude(group = "rg.slf4j", module = "jcl104-over-slf4j")
- exclude(group = "log4j", module = "log4j")
- }
"testUtilsImplementation"(sourceSets.main.get().output)
"testUtilsImplementation"("com.google.code.findbugs:annotations:3.0.0")
"testUtilsImplementation"(libs.junit)
@@ -643,7 +711,5 @@
"testUtilsImplementation"("commons-io:commons-io:2.7")
"testUtilsImplementation"("com.google.guava:guava:29.0-jre")
"testUtilsImplementation"("commons-collections:commons-collections:3.1")
-
- // Override Java 9 incompatible version (coming from displaytag):
"testUtilsImplementation"("commons-lang:commons-lang:2.6")
}
diff --git a/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt b/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
index c54c491..ddef3a3 100644
--- a/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
+++ b/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
@@ -19,6 +19,7 @@
package freemarker.build
+import java.util.concurrent.atomic.AtomicBoolean
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
@@ -29,6 +30,7 @@
import org.gradle.api.plugins.jvm.JvmTestSuiteTarget
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
@@ -36,12 +38,12 @@
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.named
+import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.setProperty
import org.gradle.kotlin.dsl.the
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.testing.base.TestingExtension
-import java.util.concurrent.atomic.AtomicBoolean
private const val TEST_UTILS_SOURCE_SET_NAME = "test-utils"
@@ -90,6 +92,7 @@
class FreemarkerModuleDef internal constructor(
private val context: JavaProjectContext,
private val ext: FreemarkerRootExtension,
+ private val generated: Boolean,
val sourceSetName: String,
val compilerVersion: JavaLanguageVersion
) {
@@ -99,27 +102,71 @@
val sourceSet = context.sourceSets.maybeCreate(sourceSetName)
val sourceSetRootDirName = "freemarker-${baseDirName}"
- val sourceSetSrcPath = "${sourceSetRootDirName}/src"
+ val sourceSetSrcPath = sourceSetRoot(context, generated, sourceSetRootDirName)
- fun enableTests(testJavaVersion: String = ext.testJavaVersion) {
- configureTests(JavaLanguageVersion.of(testJavaVersion))
+ fun generateJakartaSources(
+ baseSourceSetName: String,
+ sourceSetKind: String = SourceSet.MAIN_SOURCE_SET_NAME,
+ targetSourceSet: SourceSet = sourceSet
+ ): List<TaskProvider<JakartaSourceRootGeneratorTask>> {
+ val baseSourceSetRef = context.sourceSets.named(baseSourceSetName)
+ val taskNameClassifier = if (SourceSet.MAIN_SOURCE_SET_NAME == sourceSetKind) {
+ ""
+ } else {
+ sourceSetKind.replaceFirstChar { it.uppercaseChar() }
+ }
+
+ val generateJakartaSources = context.tasks
+ .register<JakartaSourceRootGeneratorTask>("generateJakarta${taskNameClassifier}Sources") {
+ sourceDirectory.set(baseSourceSetRef.get().java.srcDirs.single())
+ destinationDirectory.set(project.file(sourceSetSrcPath).resolve(sourceSetKind).resolve("java"))
+ }
+ targetSourceSet.java.srcDir(generateJakartaSources)
+
+ val generateJakartaResources = context.tasks
+ .register<JakartaSourceRootGeneratorTask>("generateJakarta${taskNameClassifier}Resources") {
+ sourceDirectory.set(baseSourceSetRef.get().resources.srcDirs.single())
+ destinationDirectory.set(project.file(sourceSetSrcPath).resolve(sourceSetKind).resolve("resources"))
+ }
+ targetSourceSet.resources.srcDir(generateJakartaResources)
+ return listOf(generateJakartaSources, generateJakartaResources)
}
- private fun configureTests(testJavaVersion: JavaLanguageVersion) {
- getOrCreateTestSuiteRef().configure {
+ private fun sourceSetRoot(
+ context: JavaProjectContext,
+ generated: Boolean,
+ sourceSetRootDirName: String
+ ): String {
+ return if (generated) {
+ context.project.layout.buildDirectory.get().asFile
+ .resolve("generated")
+ .resolve(sourceSetRootDirName)
+ .toString()
+ } else {
+ "${sourceSetRootDirName}/src"
+ }
+ }
+
+ fun enableTests(testJavaVersion: String = ext.testJavaVersion) =
+ configureTests(JavaLanguageVersion.of(testJavaVersion))
+
+ private fun configureTests(testJavaVersion: JavaLanguageVersion): NamedDomainObjectProvider<JvmTestSuite> {
+ val testSuitRef = getOrCreateTestSuiteRef()
+ testSuitRef.configure {
useJUnit(context.version("junit"))
configureSources(sources, testJavaVersion)
targets.all { configureTarget(this, sources, testJavaVersion) }
}
+ return testSuitRef
}
private fun getOrCreateTestSuiteRef(): NamedDomainObjectProvider<JvmTestSuite> {
val suites = context.testing.suites
- if (main) {
- return suites.named<JvmTestSuite>(JvmTestSuitePlugin.DEFAULT_TEST_SUITE_NAME)
+ return if (main) {
+ suites.named<JvmTestSuite>(JvmTestSuitePlugin.DEFAULT_TEST_SUITE_NAME)
} else {
- return suites.register("${sourceSetName}Test", JvmTestSuite::class.java)
+ suites.register("${sourceSetName}Test", JvmTestSuite::class.java)
}
}
@@ -134,9 +181,14 @@
private fun configureSources(sources: SourceSet, testJavaVersion: JavaLanguageVersion) {
sources.apply {
- val testSrcPath = "${sourceSetSrcPath}/test"
- java.setSrcDirs(listOf("${testSrcPath}/java"))
- resources.setSrcDirs(listOf("${testSrcPath}/resources"))
+ if (generated) {
+ java.setSrcDirs(emptyList<String>())
+ resources.setSrcDirs(emptyList<String>())
+ } else {
+ val testSrcPath = "${sourceSetSrcPath}/test"
+ java.setSrcDirs(listOf("${testSrcPath}/java"))
+ resources.setSrcDirs(listOf("${testSrcPath}/resources"))
+ }
if (!main) {
context.inheritCompileRuntimeAndOutput(this, sourceSet)
@@ -238,6 +290,21 @@
}
}
+ fun configureGeneratedSourceSet(
+ sourceSetName: String,
+ configuration: FreemarkerModuleDef.() -> Unit = { }
+ ) {
+ configureGeneratedSourceSet(sourceSetName, javaVersion, configuration)
+ }
+
+ fun configureGeneratedSourceSet(
+ sourceSetName: String,
+ sourceSetVersion: String,
+ configuration: FreemarkerModuleDef.() -> Unit = { }
+ ) {
+ configureSourceSet(true, sourceSetName, sourceSetVersion, configuration)
+ }
+
fun configureSourceSet(
sourceSetName: String,
configuration: FreemarkerModuleDef.() -> Unit = { }
@@ -250,18 +317,31 @@
sourceSetVersion: String,
configuration: FreemarkerModuleDef.() -> Unit = { }
) {
+ configureSourceSet(false, sourceSetName, sourceSetVersion, configuration)
+ }
+
+ private fun configureSourceSet(
+ generated: Boolean,
+ sourceSetName: String,
+ sourceSetVersion: String,
+ configuration: FreemarkerModuleDef.() -> Unit = { }
+ ) {
if (testUtilsConfigured.compareAndSet(false, true)) {
configureTestUtils()
}
allConfiguredSourceSetNamesRef.add(sourceSetName)
- FreemarkerModuleDef(context, this, sourceSetName, JavaLanguageVersion.of(sourceSetVersion)).apply {
- val sourceSetSrcMainPath = "${sourceSetSrcPath}/main"
-
+ FreemarkerModuleDef(context, this, generated, sourceSetName, JavaLanguageVersion.of(sourceSetVersion)).apply {
sourceSet.apply {
- java.setSrcDirs(listOf("${sourceSetSrcMainPath}/java"))
- resources.setSrcDirs(listOf("${sourceSetSrcMainPath}/resources"))
+ if (generated) {
+ java.setSrcDirs(emptyList<String>())
+ resources.setSrcDirs(emptyList<String>())
+ } else {
+ val sourceSetSrcMainPath = "${sourceSetSrcPath}/main"
+ java.setSrcDirs(listOf("${sourceSetSrcMainPath}/java"))
+ resources.setSrcDirs(listOf("${sourceSetSrcMainPath}/resources"))
+ }
}
if (!main) {
diff --git a/buildSrc/src/main/kotlin/freemarker/build/JakartaSourceRootGeneratorTask.kt b/buildSrc/src/main/kotlin/freemarker/build/JakartaSourceRootGeneratorTask.kt
new file mode 100644
index 0000000..a447dc0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/freemarker/build/JakartaSourceRootGeneratorTask.kt
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.build
+
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.EmptyFileVisitor
+import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.file.FileVisitDetails
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.tasks.IgnoreEmptyDirectories
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.kotlin.dsl.mapProperty
+import org.gradle.kotlin.dsl.setProperty
+
+open class JakartaSourceRootGeneratorTask @Inject constructor(
+ private val fs: FileSystemOperations,
+ objects: ObjectFactory
+) : DefaultTask() {
+
+ @InputDirectory
+ @SkipWhenEmpty
+ @IgnoreEmptyDirectories
+ @PathSensitive(PathSensitivity.RELATIVE)
+ val sourceDirectory = objects.directoryProperty()
+
+ @Input
+ val packageMappings = objects.mapProperty<String, String>()
+
+ @Input
+ val fileNameMappings = objects.mapProperty<String, String>()
+
+ @Input
+ val noAutoReplacePackages = objects.setProperty<String>().value(setOf())
+
+ @Input
+ val replacements = objects.mapProperty<String, String>()
+
+ @OutputDirectory
+ val destinationDirectory = objects.directoryProperty()
+
+ private fun toNewPath(oldPath: List<String>, origToNewPackage: Map<String, String>): List<String> {
+ for (oldPackageEndIndex in oldPath.size downTo 1) {
+ val oldPackageName = oldPath.subList(0, oldPackageEndIndex).joinToString(".")
+ val newPackageName = origToNewPackage[oldPackageName]
+ if (newPackageName != null) {
+ return newPackageName.split('.') + oldPath.subList(oldPackageEndIndex, oldPath.size)
+ }
+ }
+ return oldPath
+ }
+
+ private fun toPackagePath(packageName: String) = packageName.replace('.', '/')
+
+ private fun allReplacements(origToNewPackage: Map<String, String>): Map<String, String> {
+ val allReplacements = LinkedHashMap(origToNewPackage)
+ val skippedPackageReplacements = noAutoReplacePackages.get()
+ origToNewPackage.forEach { (origPackage, newPackage) ->
+ if (!skippedPackageReplacements.contains(origPackage)) {
+ allReplacements[toPackagePath(origPackage)] = toPackagePath(newPackage)
+ }
+ }
+ skippedPackageReplacements.forEach(allReplacements::remove)
+ allReplacements.putAll(replacements.get())
+ return allReplacements
+ }
+
+ @TaskAction
+ fun copyFiles() {
+ val fileNameMappingsCapture: Map<String, String> = fileNameMappings.get()
+ val origToNewPackage: Map<String, String> = packageMappings.get()
+
+ val allReplacements = allReplacements(origToNewPackage)
+
+ val destRoot = destinationDirectory.get().asFile
+ fs.delete { delete(destRoot) }
+
+ sourceDirectory.asFileTree.visit(object : EmptyFileVisitor() {
+ override fun visitFile(fileDetails: FileVisitDetails) {
+ val relPath = fileDetails.relativePath
+
+ val newPackage = toNewPath(relPath.parent.segments.asList(), origToNewPackage)
+
+ val srcPath = fileDetails.file
+ var fileContent = srcPath.readText()
+ allReplacements.forEach { (key, value) ->
+ fileContent = fileContent.replace(key, value)
+ }
+
+ val destName = fileNameMappingsCapture[srcPath.name] ?: srcPath.name
+ val destPath = destRoot
+ .resolve(newPackage.joinToString(File.separator))
+ .resolve(destName)
+
+ destPath.parentFile.mkdirs()
+ destPath.writeText(applyJakartaPreprocessingBasedOnName(fileContent, destName))
+ }
+ })
+ }
+
+ private fun isJakartaDirective(line: String, directive: String): Boolean {
+ if (!line.startsWith(directive)) {
+ return false
+ }
+ return line.substring(directive.length).trim() == "jakarta"
+ }
+
+ private fun lineType(line: String, lineComment: LineCommentType): SourceLineType {
+ val uncommented = lineComment
+ .uncommentIfLineComment(line)
+ ?: return SourceLineType.OTHER
+
+ val directiveLine = uncommented.trim()
+ if (isJakartaDirective(directiveLine, "#if")) {
+ return SourceLineType.IF_START
+ }
+ if (isJakartaDirective(directiveLine, "#else")) {
+ return SourceLineType.ELSE
+ }
+ if (isJakartaDirective(directiveLine, "#endif")) {
+ return SourceLineType.ENDIF
+ }
+ return SourceLineType.OTHER
+ }
+
+ private fun extension(fileName: String): String {
+ val dotIndex = fileName.lastIndexOf('.')
+ return if (dotIndex < 0) "" else fileName.substring(dotIndex + 1)
+ }
+
+ private fun applyJakartaPreprocessingBasedOnName(input: String, fileName: String): String {
+ val lineCommentType = when (extension(fileName)) {
+ "java" -> LineCommentType.C_LIKE
+ "jsp" -> LineCommentType.JSP_LIKE
+ "xml", "html", "tld" -> LineCommentType.XML_LIKE
+ else -> return input
+ }
+ return applyJakartaPreprocessing(input, lineCommentType)
+ }
+
+ private fun applyJakartaPreprocessing(input: String, lineComment: LineCommentType): String {
+ val output = StringBuilder()
+ val modified = applyJakartaPreprocessing(input, output, lineComment)
+ return if (modified) output.toString() else input
+ }
+
+ private fun applyJakartaPreprocessing(
+ input: String,
+ output: StringBuilder,
+ lineComment: LineCommentType
+ ): Boolean {
+ var modified = false
+ var mode = SourceProcessingMode.OTHER
+
+ input.lineSequence().forEach { line ->
+ when (lineType(line, lineComment)) {
+ SourceLineType.IF_START -> {
+ if (mode != SourceProcessingMode.OTHER) {
+ throw IllegalStateException("Nested #if is not supported")
+ }
+ mode = SourceProcessingMode.JAKARTA_BLOCK
+ }
+ SourceLineType.ELSE -> {
+ if (mode != SourceProcessingMode.JAKARTA_BLOCK) {
+ throw IllegalStateException("Unexpected #else")
+ }
+ mode = SourceProcessingMode.NON_JAKARTA_BLOCK
+ }
+ SourceLineType.ENDIF -> {
+ if (mode == SourceProcessingMode.OTHER) {
+ throw IllegalStateException("Unexpected #endif")
+ }
+ mode = SourceProcessingMode.OTHER
+ }
+ SourceLineType.OTHER -> {
+ when (mode) {
+ SourceProcessingMode.JAKARTA_BLOCK -> {
+ modified = true
+ output.append(lineComment.uncomment(line))
+ output.append('\n')
+ }
+ SourceProcessingMode.NON_JAKARTA_BLOCK -> {
+ modified = true
+ }
+ SourceProcessingMode.OTHER -> {
+ output.append(line)
+ output.append('\n')
+ }
+ }
+ }
+ }
+ }
+ if (mode != SourceProcessingMode.OTHER) {
+ throw IllegalStateException("Unterminated #if")
+ }
+ return modified
+ }
+
+ private enum class SourceProcessingMode {
+ JAKARTA_BLOCK, NON_JAKARTA_BLOCK, OTHER
+ }
+
+ private enum class SourceLineType {
+ IF_START, ELSE, ENDIF, OTHER
+ }
+
+ private enum class LineCommentType {
+ C_LIKE {
+ override fun uncommentIfLineComment(line: String): String? =
+ uncommentIfLineComment(line, "//")
+ },
+ XML_LIKE {
+ override fun uncommentIfLineComment(line: String): String? =
+ uncommentIfLineComment(line, "<!--", "-->")
+ },
+ JSP_LIKE {
+ override fun uncommentIfLineComment(line: String): String? =
+ uncommentIfLineComment(line, "<%--", "--%>")
+ };
+
+ protected fun uncommentIfLineComment(line: String, commentOpen: String): String? {
+ val commentIndex = line.indexOf(commentOpen)
+ if (commentIndex < 0) {
+ return null
+ }
+
+ val preCommentLine = line.substring(0, commentIndex)
+ if (preCommentLine.trim().isNotEmpty()) {
+ return null
+ }
+ return preCommentLine + line.substring(commentIndex + commentOpen.length)
+ }
+
+ protected fun uncommentIfLineComment(line: String, commentOpen: String, commentClose: String): String? {
+ val noOpenLine = uncommentIfLineComment(line, commentOpen)
+ ?: return null
+
+ val commentCloseIndex = noOpenLine.lastIndexOf(commentClose)
+ if (commentCloseIndex < 0) {
+ return null
+ }
+ if (noOpenLine.substring(commentCloseIndex + commentClose.length).trim().isNotEmpty()) {
+ return null
+ }
+ return noOpenLine.substring(0, commentCloseIndex)
+ }
+
+ abstract fun uncommentIfLineComment(line: String): String?
+
+ fun uncomment(line: String): String =
+ uncommentIfLineComment(line) ?: throw IllegalArgumentException("Not a line comment: $line")
+ }
+}
diff --git a/freemarker-core/src/main/java/freemarker/cache/TemplateLoaderUtils.java b/freemarker-core/src/main/java/freemarker/cache/TemplateLoaderUtils.java
index 9866232..af47ad0 100644
--- a/freemarker-core/src/main/java/freemarker/cache/TemplateLoaderUtils.java
+++ b/freemarker-core/src/main/java/freemarker/cache/TemplateLoaderUtils.java
@@ -21,7 +21,7 @@
import freemarker.template.Configuration;
-final class TemplateLoaderUtils {
+public final class TemplateLoaderUtils {
private TemplateLoaderUtils() {
// Not meant to be instantiated
diff --git a/freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java b/freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java
index f6f44ee..bc5c885 100644
--- a/freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java
+++ b/freemarker-core/src/main/java/freemarker/cache/URLTemplateSource.java
@@ -29,7 +29,7 @@
/**
* Wraps a {@link URL}, and implements methods required for a typical template source.
*/
-class URLTemplateSource {
+public class URLTemplateSource {
private final URL url;
private URLConnection conn;
private InputStream inputStream;
@@ -38,7 +38,7 @@
/**
* @param useCaches {@code null} if this aspect wasn't set in the parent {@link TemplateLoader}.
*/
- URLTemplateSource(URL url, Boolean useCaches) throws IOException {
+ public URLTemplateSource(URL url, Boolean useCaches) throws IOException {
this.url = url;
this.conn = url.openConnection();
this.useCaches = useCaches;
@@ -66,7 +66,7 @@
return url.toString();
}
- long lastModified() {
+ public long lastModified() {
if (conn instanceof JarURLConnection) {
// There is a bug in sun's jar url connection that causes file handle leaks when calling getLastModified()
// (see https://bugs.openjdk.java.net/browse/JDK-6956385).
@@ -103,7 +103,7 @@
}
}
- InputStream getInputStream() throws IOException {
+ public InputStream getInputStream() throws IOException {
if (inputStream != null) {
// Ensure that the returned InputStream reads from the beginning of the resource when getInputStream()
// is called for the second time:
@@ -118,7 +118,7 @@
return inputStream;
}
- void close() throws IOException {
+ public void close() throws IOException {
try {
if (inputStream != null) {
inputStream.close();
diff --git a/freemarker-javax-servlet/src/main/java/freemarker/cache/WebappTemplateLoader.java b/freemarker-javax-servlet/src/main/java/freemarker/cache/WebappTemplateLoader.java
index 8ff7b6c..ddd654f 100644
--- a/freemarker-javax-servlet/src/main/java/freemarker/cache/WebappTemplateLoader.java
+++ b/freemarker-javax-servlet/src/main/java/freemarker/cache/WebappTemplateLoader.java
@@ -19,6 +19,10 @@
package freemarker.cache;
+// #if jakarta
+//import freemarker.cache.*;
+// #endif jakarta
+
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
diff --git a/freemarker-javax-servlet/src/test/java/freemarker/template/MockServletContext.java b/freemarker-javax-servlet/src/test/java/freemarker/template/MockServletContext.java
index 0ed47ec..58ccf6d 100644
--- a/freemarker-javax-servlet/src/test/java/freemarker/template/MockServletContext.java
+++ b/freemarker-javax-servlet/src/test/java/freemarker/template/MockServletContext.java
@@ -255,15 +255,15 @@
}
public void log(String arg0) {
-
+
}
public void log(Exception arg0, String arg1) {
-
+
}
public void log(String arg0, Throwable arg1) {
-
+
}
public void removeAttribute(String arg0) {
@@ -271,5 +271,41 @@
public void setAttribute(String arg0, Object arg1) {
}
-
-}
\ No newline at end of file
+
+// #if jakarta
+// @Override
+// public ServletRegistration.Dynamic addJspFile(String s, String s1) {
+// return null;
+// }
+//
+// @Override
+// public int getSessionTimeout() {
+// return 0;
+// }
+//
+// @Override
+// public void setSessionTimeout(int i) {
+//
+// }
+//
+// @Override
+// public String getRequestCharacterEncoding() {
+// return null;
+// }
+//
+// @Override
+// public void setRequestCharacterEncoding(String s) {
+//
+// }
+//
+// @Override
+// public String getResponseCharacterEncoding() {
+// return null;
+// }
+//
+// @Override
+// public void setResponseCharacterEncoding(String s) {
+//
+// }
+// #endif jakarta
+}
diff --git a/freemarker-javax-servlet/src/test/java/freemarker/test/servlet/WebAppTestCase.java b/freemarker-javax-servlet/src/test/java/freemarker/test/servlet/WebAppTestCase.java
index ab14fb2..c69e0e1 100644
--- a/freemarker-javax-servlet/src/test/java/freemarker/test/servlet/WebAppTestCase.java
+++ b/freemarker-javax-servlet/src/test/java/freemarker/test/servlet/WebAppTestCase.java
@@ -237,7 +237,7 @@
// Pattern of jar file names scanned for META-INF/*.tld:
context.setAttribute(
ATTR_JETTY_CONTAINER_INCLUDE_JAR_PATTERN,
- ".*taglib.*\\.jar$");
+ ".*(taglib|jsp\\.jstl).*\\.jar$");
addJasperInitializer(context);