diff --git a/platform/built-in-server-api/src/org/jetbrains/builtInWebServer/PathInfo.kt b/platform/built-in-server-api/src/org/jetbrains/builtInWebServer/PathInfo.kt index 366e080e87f4..324dbc0ef2f1 100644 --- a/platform/built-in-server-api/src/org/jetbrains/builtInWebServer/PathInfo.kt +++ b/platform/built-in-server-api/src/org/jetbrains/builtInWebServer/PathInfo.kt @@ -7,10 +7,13 @@ import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import java.io.File -class PathInfo(val ioFile: File?, val file: VirtualFile?, val root: VirtualFile, moduleName: String? = null, private val isLibrary: Boolean = false) { +class PathInfo(val ioFile: File?, val file: VirtualFile?, val root: VirtualFile, moduleName: String? = null, val isLibrary: Boolean = false) { var moduleName: String? = moduleName set + /** + * URL path. + */ val path: String by lazy { val builder = StringBuilder() if (moduleName != null) { @@ -30,6 +33,11 @@ class PathInfo(val ioFile: File?, val file: VirtualFile?, val root: VirtualFile, builder.toString() } + /** + * System-dependent path to file. + */ + val filePath: String by lazy { if (ioFile == null) FileUtilRt.toSystemDependentName(file!!.path) else ioFile.path } + val isValid: Boolean get() = if (ioFile == null) file!!.isValid else ioFile.exists() @@ -38,4 +46,8 @@ class PathInfo(val ioFile: File?, val file: VirtualFile?, val root: VirtualFile, val fileType: FileType get() = if (ioFile == null) file!!.fileType else FileTypeManager.getInstance().getFileTypeByFileName(ioFile.name) + + + val isDirectory: Boolean + get() = if (ioFile == null) file!!.isDirectory else ioFile.isDirectory } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 868d1f0b8393..924ea537a420 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -49,31 +49,29 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { return false } } - else { - var virtualFile = pathInfo.file - val isDirectory = if (virtualFile == null) pathInfo.ioFile!!.isDirectory else virtualFile.isDirectory - if (isDirectory) { - if (!endsWithSlash(decodedRawPath)) { - WebServerPathHandler.redirectToDirectory(request, channel, if (isCustomHost) path else (projectName + '/' + path)) - return true - } - - if (virtualFile == null) { - virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(pathInfo.ioFile!!) - } - - virtualFile = if (virtualFile == null) null else findIndexFile(virtualFile) - if (virtualFile == null) { - Responses.sendStatus(HttpResponseStatus.NOT_FOUND, channel, "Index file doesn't exist.", request) - return true - } - indexUsed = true - } - } pathToFileManager.pathToInfoCache.put(path, pathInfo) } - else if (!endsWithName(path, pathInfo.name)) { + + if (pathInfo.isDirectory) { + if (!endsWithSlash(decodedRawPath)) { + WebServerPathHandler.redirectToDirectory(request, channel, if (isCustomHost) path else ("$projectName/$path")) + return true + } + + var virtualFile = pathInfo.file ?: LocalFileSystem.getInstance().refreshAndFindFileByIoFile(pathInfo.ioFile!!) + virtualFile = if (virtualFile == null) null else findIndexFile(virtualFile) + if (virtualFile == null) { + Responses.sendStatus(HttpResponseStatus.NOT_FOUND, channel, "Index file doesn't exist.", request) + return true + } + + indexUsed = true + pathInfo = PathInfo(null, virtualFile, pathInfo.root, pathInfo.moduleName, pathInfo.isLibrary) + pathToFileManager.pathToInfoCache.put(path, pathInfo) + } + + if (!indexUsed && !endsWithName(path, pathInfo.name)) { if (endsWithSlash(decodedRawPath)) { indexUsed = true } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.kt index 50446a460b24..cb8cda2b4cc0 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerRootsProvider.kt @@ -67,10 +67,12 @@ class DefaultWebServerRootsProvider : WebServerRootsProvider() { val modules = runReadAction { ModuleManager.getInstance(project).modules } val resolver = WebServerPathToFileManager.getInstance(project).getResolver(effectivePath) - for (rootProvider in RootProvider.values()) { - val result = findByRelativePath(project, effectivePath, modules, rootProvider, resolver) - if (result != null) { - return result + if (!modules.isEmpty()) { + for (rootProvider in RootProvider.values()) { + val result = findByRelativePath(project, effectivePath, modules, rootProvider, resolver) + if (result != null) { + return result + } } } return findInLibraries(project, modules, effectivePath, resolver) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.kt index 4e6e1050831a..22bdcb021615 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathToFileManager.kt @@ -61,7 +61,7 @@ class WebServerPathToFileManager(application: Application, private val project: return pathInfo.file ?: return LocalFileSystem.getInstance().findFileByIoFile(pathInfo.ioFile!!) } - fun getPathInfo(path: String, cacheResult: Boolean = true): PathInfo? { + @JvmOverloads fun getPathInfo(path: String, cacheResult: Boolean = true): PathInfo? { var pathInfo = pathToInfoCache.getIfPresent(path) if (pathInfo == null || !pathInfo.isValid) { pathInfo = doFindByRelativePath(path) diff --git a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiRequest.kt b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiRequest.kt index fab42264c91a..3b4a271f40b9 100644 --- a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiRequest.kt +++ b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiRequest.kt @@ -33,7 +33,7 @@ class FastCgiRequest(val requestId: Int, allocator: ByteBufAllocator) { fun writeFileHeaders(pathInfo: PathInfo, canonicalRequestPath: CharSequence) { val root = pathInfo.root addHeader("DOCUMENT_ROOT", root.path.separatorsToSystem()) - addHeader("SCRIPT_FILENAME", if (pathInfo.ioFile == null) pathInfo.file!!.path.separatorsToSystem() else pathInfo.ioFile!!.path) + addHeader("SCRIPT_FILENAME", pathInfo.filePath) addHeader("SCRIPT_NAME", canonicalRequestPath) } diff --git a/platform/built-in-server/testSrc/BuiltInServerTestCase.kt b/platform/built-in-server/testSrc/BuiltInServerTestCase.kt new file mode 100644 index 000000000000..58e83935b6fc --- /dev/null +++ b/platform/built-in-server/testSrc/BuiltInServerTestCase.kt @@ -0,0 +1,57 @@ +package org.jetbrains.ide + +import com.intellij.testFramework.DisposeModulesRule +import com.intellij.testFramework.ProjectRule +import com.intellij.testFramework.RuleChain +import com.intellij.testFramework.TemporaryDirectory +import io.netty.handler.codec.http.HttpResponseStatus +import org.assertj.core.api.Assertions.assertThat +import org.junit.ClassRule +import org.junit.Rule +import org.junit.rules.Timeout +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.TimeUnit + +internal abstract class BuiltInServerTestCase { + companion object { + @ClassRule val projectRule = ProjectRule() + } + + protected val tempDirManager = TemporaryDirectory() + protected val manager = TestManager(projectRule, tempDirManager) + + private val ruleChain = RuleChain( + tempDirManager, + Timeout(60, TimeUnit.SECONDS), + manager, + DisposeModulesRule(projectRule)) + @Rule fun getChain() = ruleChain + + protected open val urlPathPrefix = "" + + protected fun doTest(filePath: String? = manager.filePath, additionalCheck: ((connection: HttpURLConnection) -> Unit)? = null) { + val serviceUrl = "http://localhost:${BuiltInServerManager.getInstance().port}$urlPathPrefix" + var url = serviceUrl + (if (filePath == null) "" else ("/$filePath")) + val line = manager.annotation?.line ?: -1 + if (line != -1) { + url += ":$line" + } + val column = manager.annotation?.column ?: -1 + if (column != -1) { + url += ":$column" + } + + val connection = URL(url).openConnection() as HttpURLConnection + val expectedStatus = HttpResponseStatus.valueOf(manager.annotation?.status ?: 200) + assertThat(HttpResponseStatus.valueOf(connection.responseCode)).isEqualTo(expectedStatus) + + check(serviceUrl, expectedStatus) + if (additionalCheck != null) { + additionalCheck(connection) + } + } + + protected open fun check(serviceUrl: String, expectedStatus: HttpResponseStatus) { + } +} \ No newline at end of file diff --git a/platform/built-in-server/testSrc/BuiltInWebServerTest.kt b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt new file mode 100644 index 000000000000..6294e4243fb9 --- /dev/null +++ b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt @@ -0,0 +1,56 @@ +package org.jetbrains.ide + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.module.EmptyModuleType +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.testFramework.refreshVfs +import com.intellij.testFramework.runInEdtAndWait +import com.intellij.testFramework.systemIndependentPath +import com.intellij.testFramework.writeChild +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +private class BuiltInWebServerTest : BuiltInServerTestCase() { + override val urlPathPrefix: String + get() = "/${BuiltInServerTestCase.projectRule.project.name}" + + @Test + @TestManager.TestDescriptor(filePath = "foo/index.html", doNotCreate = true, status = 200) + fun `get only dir without end slash`() { + testIndex("foo") + } + + @Test + @TestManager.TestDescriptor(filePath = "foo/index.html", doNotCreate = true, status = 200) + fun `get only dir with end slash`() { + testIndex("foo/") + } + + @Test + @TestManager.TestDescriptor(filePath = "foo/index.html", doNotCreate = true, status = 200) + fun `get index file and then dir`() { + testIndex("foo/index.html", "foo") + } + + private fun testIndex(vararg paths: String) { + val project = BuiltInServerTestCase.projectRule.project + val newPath = tempDirManager.newPath(refreshVfs = false) + newPath.writeChild(manager.filePath!!, "hello") + newPath.refreshVfs() + + runInEdtAndWait { + runWriteAction { + val systemIndependentPath = newPath.systemIndependentPath + val module = ModuleManager.getInstance(project).newModule("$systemIndependentPath/test.iml", EmptyModuleType.EMPTY_MODULE) + ModuleRootModificationUtil.addContentRoot(module, systemIndependentPath) + } + } + + for (path in paths) { + doTest(path) { + assertThat(it.inputStream.reader().readText()).isEqualTo("hello") + } + } + } +} \ No newline at end of file diff --git a/platform/built-in-server/testSrc/RestApiTest.kt b/platform/built-in-server/testSrc/RestApiTest.kt index eddd7ae66206..f399816c1be9 100644 --- a/platform/built-in-server/testSrc/RestApiTest.kt +++ b/platform/built-in-server/testSrc/RestApiTest.kt @@ -2,32 +2,17 @@ package org.jetbrains.ide import com.google.gson.stream.JsonWriter import com.intellij.openapi.vfs.CharsetToolkit -import com.intellij.testFramework.ProjectRule -import com.intellij.testFramework.RuleChain -import com.intellij.testFramework.TemporaryDirectory import io.netty.handler.codec.http.HttpResponseStatus import org.assertj.core.api.Assertions.assertThat import org.jetbrains.ide.TestManager.TestDescriptor -import org.junit.ClassRule -import org.junit.Rule import org.junit.Test -import org.junit.rules.Timeout import java.io.BufferedOutputStream import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.net.URL -import java.util.concurrent.TimeUnit -class RestApiTest { - companion object { - @ClassRule val projectRule = ProjectRule() - } - - private val tempDirManager = TemporaryDirectory() - private val manager = TestManager(projectRule, tempDirManager) - - private val ruleChain = RuleChain(tempDirManager, Timeout(60, TimeUnit.SECONDS), manager) - @Rule fun getChain() = ruleChain +private class RestApiTest : BuiltInServerTestCase() { + override val urlPathPrefix = "/api/file" @Test @TestDescriptor(filePath = "", status = 400) @@ -77,23 +62,11 @@ class RestApiTest { doTest() } - private fun doTest() { - val serviceUrl = "http://localhost:${BuiltInServerManager.getInstance().port}/api/file" - var url = serviceUrl + (if (manager.filePath == null) "" else ("/${manager.filePath}")) + override fun check(serviceUrl: String, expectedStatus: HttpResponseStatus) { val line = manager.annotation?.line ?: -1 - if (line != -1) { - url += ":$line" - } val column = manager.annotation?.column ?: -1 - if (column != -1) { - url += ":$column" - } - var connection = URL(url).openConnection() as HttpURLConnection - val expectedStatus = HttpResponseStatus.valueOf(manager.annotation?.status ?: 200) - assertThat(HttpResponseStatus.valueOf(connection.responseCode)).isEqualTo(expectedStatus) - - connection = URL("$serviceUrl?file=${manager.filePath ?: ""}&line=$line&column=$column").openConnection() as HttpURLConnection + var connection = URL("$serviceUrl?file=${manager.filePath ?: ""}&line=$line&column=$column").openConnection() as HttpURLConnection assertThat(HttpResponseStatus.valueOf(connection.responseCode)).isEqualTo(expectedStatus) connection = URL("$serviceUrl").openConnection() as HttpURLConnection @@ -109,3 +82,4 @@ class RestApiTest { assertThat(HttpResponseStatus.valueOf(connection.responseCode)).isEqualTo(expectedStatus) } } + diff --git a/platform/built-in-server/testSrc/TestManager.kt b/platform/built-in-server/testSrc/TestManager.kt index a022f47f79e4..f6ed81702cee 100644 --- a/platform/built-in-server/testSrc/TestManager.kt +++ b/platform/built-in-server/testSrc/TestManager.kt @@ -10,16 +10,10 @@ import com.intellij.testFramework.* import org.junit.rules.TestWatcher import org.junit.runner.Description import java.io.File -import java.lang.annotation.ElementType -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy -import java.lang.annotation.Target -class TestManager(val projectRule: ProjectRule, private val tempDirManager: TemporaryDirectory) : TestWatcher() { - companion object { - private val EXCLUDED_DIR_NAME = "excludedDir" - } +private val EXCLUDED_DIR_NAME = "excludedDir" +internal class TestManager(val projectRule: ProjectRule, private val tempDirManager: TemporaryDirectory) : TestWatcher() { var annotation: TestDescriptor? = null var filePath: String? = null @@ -27,15 +21,14 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp private var ioFileToDelete: File? = null - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - annotation public class TestDescriptor(public val filePath: String, - public val line: Int = -1, - public val column: Int = -1, - public val relativeToProject: Boolean = false, - public val excluded: Boolean = false, - public val doNotCreate: Boolean = false, - public val status: Int = 200) + @Target(AnnotationTarget.FUNCTION) + annotation class TestDescriptor(val filePath: String, + val line: Int = -1, + val column: Int = -1, + val relativeToProject: Boolean = false, + val excluded: Boolean = false, + val doNotCreate: Boolean = false, + val status: Int = 200) override fun starting(description: Description) { annotation = description.getAnnotation(TestDescriptor::class.java) @@ -49,9 +42,7 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp } // trigger project creation - runInEdtAndWait { - projectRule.project - } + projectRule.project if (filePath!! == "_tmp_") { val file = tempDirManager.newPath(".txt") @@ -69,7 +60,7 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp runInEdtAndWait { val normalizedFilePath = FileUtilRt.toSystemIndependentName(filePath!!) if (annotation!!.relativeToProject) { - val root = projectRule.project.getBaseDir() + val root = projectRule.project.baseDir runWriteAction { fileToDelete = root.findOrCreateChildData(this@TestManager, normalizedFilePath) } @@ -78,8 +69,8 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp val module = projectRule.module if (annotation!!.excluded) { ModuleRootModificationUtil.updateModel(module) { model -> - val contentEntry = model.getContentEntries()[0] - val contentRoot = contentEntry.getFile()!! + val contentEntry = model.contentEntries[0] + val contentRoot = contentEntry.file!! runWriteAction { contentRoot.findChild(EXCLUDED_DIR_NAME)?.delete(this@TestManager) fileToDelete = contentRoot.createChildDirectory(this@TestManager, EXCLUDED_DIR_NAME) @@ -91,7 +82,7 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp filePath = "$EXCLUDED_DIR_NAME/$filePath" } else { - val root = ModuleRootManager.getInstance(module).getSourceRoots()[0] + val root = ModuleRootManager.getInstance(module).sourceRoots[0] runWriteAction { fileToDelete = root.findOrCreateChildData(this@TestManager, normalizedFilePath) } @@ -102,7 +93,7 @@ class TestManager(val projectRule: ProjectRule, private val tempDirManager: Temp override fun finished(description: Description?) { if (annotation!!.excluded) { - ModuleRootModificationUtil.updateModel(projectRule.module) { model -> model.getContentEntries()[0].removeExcludeFolder(EXCLUDED_DIR_NAME) } + ModuleRootModificationUtil.updateModel(projectRule.module) { model -> model.contentEntries[0].removeExcludeFolder(EXCLUDED_DIR_NAME) } } if (fileToDelete != null) { diff --git a/platform/testFramework/test-framework-java8/FixtureRule.kt b/platform/testFramework/test-framework-java8/FixtureRule.kt index 332adba765af..e4bca84c931b 100644 --- a/platform/testFramework/test-framework-java8/FixtureRule.kt +++ b/platform/testFramework/test-framework-java8/FixtureRule.kt @@ -46,6 +46,8 @@ import java.io.PrintStream import java.nio.file.Files import java.util.concurrent.atomic.AtomicBoolean +private var sharedModule: Module? = null + /** * Project created on request, so, could be used as a bare (only application). */ @@ -56,7 +58,6 @@ class ProjectRule() : ExternalResource() { } private var sharedProject: ProjectEx? = null - private var sharedModule: Module? = null private val projectOpened = AtomicBoolean() private fun createLightProject(): ProjectEx { @@ -234,30 +235,34 @@ inline fun Project.runInLoadComponentStateMode(task: () -> T): T { class DisposeModulesRule(private val projectRule: ProjectRule) : ExternalResource() { override fun after() { projectRule.projectIfOpened?.let { - var errors: MutableList? = null val moduleManager = ModuleManager.getInstance(it) runInEdtAndWait { - for (module in moduleManager.modules) { - if (module.isDisposed) { - continue - } - - try { - moduleManager.disposeModule(module) - } - catch (e: Throwable) { - if (errors == null) { - errors = SmartList() - } - errors!!.add(e) + moduleManager.modules.forEachGuaranteed { + if (!it.isDisposed && it !== sharedModule) { + moduleManager.disposeModule(it) } } } - CompoundRuntimeException.throwIfNotEmpty(errors) } } } +inline fun Array.forEachGuaranteed(operation: (T) -> Unit): Unit { + var errors: MutableList? = null + for (element in this) { + try { + operation(element) + } + catch (e: Throwable) { + if (errors == null) { + errors = SmartList() + } + errors.add(e) + } + } + CompoundRuntimeException.throwIfNotEmpty(errors) +} + /** * Only and only if "before" logic in case of exception doesn't require "after" logic - must be no side effects if "before" finished abnormally. * So, should be one task per rule. diff --git a/platform/testFramework/test-framework-java8/TemporaryDirectory.kt b/platform/testFramework/test-framework-java8/TemporaryDirectory.kt index e6ea2eb75cac..f6eb1edfe14f 100644 --- a/platform/testFramework/test-framework-java8/TemporaryDirectory.kt +++ b/platform/testFramework/test-framework-java8/TemporaryDirectory.kt @@ -58,9 +58,9 @@ class TemporaryDirectory : ExternalResource() { /** * Directory is not created. */ - public fun newDirectory(directoryName: String? = null): File = generatePath(directoryName).toFile() + fun newDirectory(directoryName: String? = null): File = generatePath(directoryName).toFile() - public fun newPath(directoryName: String? = null, refreshVfs: Boolean = true): Path { + fun newPath(directoryName: String? = null, refreshVfs: Boolean = true): Path { val path = generatePath(directoryName) if (refreshVfs) { path.refreshVfs() @@ -79,7 +79,7 @@ class TemporaryDirectory : ExternalResource() { return path } - public fun newVirtualDirectory(directoryName: String? = null): VirtualFile { + fun newVirtualDirectory(directoryName: String? = null): VirtualFile { val path = generatePath(directoryName) path.createDirectories() val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.systemIndependentPath) @@ -88,7 +88,7 @@ class TemporaryDirectory : ExternalResource() { } } -public fun generateTemporaryPath(fileName: String?): Path { +fun generateTemporaryPath(fileName: String?): Path { val tempDirectory = Paths.get(FileUtilRt.getTempDirectory()) var path = tempDirectory.resolve(fileName) var i = 0