From c88ed780669c53ce3979697bf4173cf53d199e6d Mon Sep 17 00:00:00 2001 From: Attila Kelemen Date: Wed, 27 Dec 2023 20:39:05 +0100 Subject: [PATCH] Added Jakarta sources generation. --- .github/workflows/ci.yml | 6 +- README.md | 2 +- build.gradle.kts | 96 +++++- .../build/FreemarkerRootExtension.kt | 114 +++++-- .../build/JakartaSourceRootGeneratorTask.kt | 277 ++++++++++++++++++ .../freemarker/cache/TemplateLoaderUtils.java | 2 +- .../freemarker/cache/URLTemplateSource.java | 10 +- .../cache/WebappTemplateLoader.java | 4 + .../template/MockServletContext.java | 46 ++- .../test/servlet/WebAppTestCase.java | 2 +- 10 files changed, 513 insertions(+), 46 deletions(-) create mode 100644 buildSrc/src/main/kotlin/freemarker/build/JakartaSourceRootGeneratorTask.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef7730d72..0f7ae19a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,11 @@ jobs: 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 @@ jobs: name: test-reports-${{ matrix.os }} path: build/reports/** retention-days: 30 - diff --git a/README.md b/README.md index e28262868..384c486ad 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ If you haven't yet, download the source release, or checkout FreeMarker from 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 6e6f1f933..84f2c3bfe 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 @@ freemarkerRoot { 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("compileJavacc") { @@ -127,6 +157,10 @@ configurations { 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 @@ tasks.named(JavaPlugin.JAVADOC_TASK_NAME) { 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 createBuildInfo = tasks.register("createBuildInfo") { 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 @@ eclipse { 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 @@ dependencies { 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 @@ dependencies { "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 @@ dependencies { "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 @@ dependencies { "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 c54c491ff..ddef3a34d 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.JvmTestSuite 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.JavaLanguageVersion 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 @@ internal class JavaProjectContext constructor( 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 @@ class FreemarkerModuleDef internal constructor( val sourceSet = context.sourceSets.maybeCreate(sourceSetName) val sourceSetRootDirName = "freemarker-${baseDirName}" - val sourceSetSrcPath = "${sourceSetRootDirName}/src" + val sourceSetSrcPath = sourceSetRoot(context, generated, sourceSetRootDirName) + + fun generateJakartaSources( + baseSourceSetName: String, + sourceSetKind: String = SourceSet.MAIN_SOURCE_SET_NAME, + targetSourceSet: SourceSet = sourceSet + ): List> { + val baseSourceSetRef = context.sourceSets.named(baseSourceSetName) + val taskNameClassifier = if (SourceSet.MAIN_SOURCE_SET_NAME == sourceSetKind) { + "" + } else { + sourceSetKind.replaceFirstChar { it.uppercaseChar() } + } - fun enableTests(testJavaVersion: String = ext.testJavaVersion) { - configureTests(JavaLanguageVersion.of(testJavaVersion)) + val generateJakartaSources = context.tasks + .register("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("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 { + val testSuitRef = getOrCreateTestSuiteRef() + testSuitRef.configure { useJUnit(context.version("junit")) configureSources(sources, testJavaVersion) targets.all { configureTarget(this, sources, testJavaVersion) } } + return testSuitRef } private fun getOrCreateTestSuiteRef(): NamedDomainObjectProvider { val suites = context.testing.suites - if (main) { - return suites.named(JvmTestSuitePlugin.DEFAULT_TEST_SUITE_NAME) + return if (main) { + suites.named(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 @@ class FreemarkerModuleDef internal constructor( 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()) + resources.setSrcDirs(emptyList()) + } 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 @@ class FreemarkerRootExtension constructor( } } + 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 = { } @@ -249,6 +316,15 @@ class FreemarkerRootExtension constructor( sourceSetName: String, 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() @@ -256,12 +332,16 @@ class FreemarkerRootExtension constructor( 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()) + resources.setSrcDirs(emptyList()) + } 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 000000000..a447dc04e --- /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() + + @Input + val fileNameMappings = objects.mapProperty() + + @Input + val noAutoReplacePackages = objects.setProperty().value(setOf()) + + @Input + val replacements = objects.mapProperty() + + @OutputDirectory + val destinationDirectory = objects.directoryProperty() + + private fun toNewPath(oldPath: List, origToNewPackage: Map): List { + 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): Map { + 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 = fileNameMappings.get() + val origToNewPackage: Map = 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 9866232c2..af47ad0c6 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 f6f44ee76..bc5c885ae 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 @@ class URLTemplateSource { /** * @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 @@ public String toString() { 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 @@ long lastModified() { } } - 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 @@ InputStream getInputStream() throws IOException { 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 8ff7b6c70..ddd654ffd 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 0ed47ec0a..58ccf6de9 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 Enumeration getServlets() { } 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 removeAttribute(String arg0) { 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 ab14fb289..c69e0e1bf 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 @@ private synchronized void ensureWebAppIsDeployed(String webAppName) throws Excep // 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);