diff --git a/src/main/kotlin/com/asyncapi/plugin/idea/_core/SchemaHtmlRenderer.kt b/src/main/kotlin/com/asyncapi/plugin/idea/_core/SchemaHtmlRenderer.kt new file mode 100644 index 0000000..b13e9d6 --- /dev/null +++ b/src/main/kotlin/com/asyncapi/plugin/idea/_core/SchemaHtmlRenderer.kt @@ -0,0 +1,111 @@ +package com.asyncapi.plugin.idea._core + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.intellij.json.JsonFileType +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Urls +import org.jetbrains.ide.BuiltInServerManager +import org.jetbrains.yaml.YAMLFileType +import java.io.File + +class SchemaHtmlRenderer { + + private val schemaTemplateUrl = "/ui/index.html" + private val serverManager = BuiltInServerManager.getInstance() + + fun render(schemaUrl: String?): String { + schemaUrl ?: return "schema: not found." + + val schemaFile = File(schemaUrl) + if (!schemaFile.exists()) { + return "schema: $schemaUrl not found." + } + + val schemaVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(schemaFile) + schemaVirtualFile ?: return "schema: $schemaUrl not found." + if (schemaVirtualFile.fileType !is YAMLFileType && schemaVirtualFile.fileType !is JsonFileType) { + return "schema: $schemaUrl not in json or yaml format." + } + + val isJson = schemaVirtualFile.fileType is JsonFileType + val schema = replaceLocalReferences(schemaFile.readText(Charsets.UTF_8), schemaVirtualFile, isJson) + val temporalSchemaUrl = saveAsTemporalFile(schema, isJson) + + val schemaTemplate = this.javaClass.getResource(schemaTemplateUrl) + schemaTemplate ?: return "schema template not found." + + return schemaTemplate.readText(Charsets.UTF_8) +// .replace( +// "schema: ``,", +// "schema: `${schema.removePrefix("\"").removeSuffix("\"")}`," +// ) + .replace( + "url: '',", + "url: '${urlToRequestedSchema(temporalSchemaUrl)}'," + ) + } + + private fun replaceLocalReferences(schema: String, schemaFile: VirtualFile, isJson: Boolean): String { + val objectMapper = if (isJson) { + ObjectMapper() + } else { + ObjectMapper(YAMLFactory()) + } + + val tree = objectMapper.readTree(schema) + tree.findParents("\$ref").forEach { + val referenceValue = (it as ObjectNode).get("\$ref") + if (referenceValue.toPrettyString().startsWith("\"./") || referenceValue.toPrettyString().startsWith("\"../")) { + (it).put("\$ref", localReferenceToFileUrl(referenceValue.toPrettyString(), schemaFile)) + } + } + + return objectMapper.writeValueAsString(tree) + } + + private fun localReferenceToFileUrl(localReference: String, schemaFile: VirtualFile): String { + val rawFileReference = localReference.removePrefix("\"").removeSuffix("\"") + val fileReference = rawFileReference.split("#/").getOrNull(0) + val schemaReference = rawFileReference.split("#/").getOrNull(1) + fileReference ?: return rawFileReference + + val referencedFile = schemaFile.parent.findFileByRelativePath(fileReference) + referencedFile ?: return fileReference + + return urlToReferencedFile(referencedFile.path, schemaReference) + } + + private fun urlToReferencedFile(fileUrl: String, schemaReference: String?): String { + val url = if (schemaReference != null) { + Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?referenceUrl=$fileUrl#/$schemaReference") + } else { + Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?referenceUrl=$fileUrl") + } + + return serverManager.addAuthToken(url!!).toExternalForm() + } + + private fun urlToRequestedSchema(schemaUrl: String): String { + val url = Urls.parseEncoded("http://localhost:${serverManager.port}/asyncapi/resources?schemaUrl=$schemaUrl") + + return serverManager.addAuthToken(url!!).toExternalForm() + } + + private fun saveAsTemporalFile(schema: String, isJson: Boolean): String { + val suffix = if (isJson) { + ".json" + } else { + ".yaml" + } + + val tempSchema = FileUtil.createTempFile("jasyncapi-idea-plugin-${System.currentTimeMillis()}", suffix, true) + tempSchema.writeText(schema, Charsets.UTF_8) + + return tempSchema.path + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/AsyncAPIBrowserUrlProvider.kt b/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/AsyncAPIBrowserUrlProvider.kt new file mode 100644 index 0000000..690cecf --- /dev/null +++ b/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/AsyncAPIBrowserUrlProvider.kt @@ -0,0 +1,31 @@ +package com.asyncapi.plugin.idea.extensions.web + +import com.asyncapi.plugin.idea.extensions.inspection.AsyncAPISchemaDetector +import com.intellij.ide.browsers.OpenInBrowserRequest +import com.intellij.ide.browsers.WebBrowserUrlProvider +import com.intellij.json.psi.JsonFile +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile +import com.intellij.util.Url +import org.jetbrains.yaml.psi.YAMLFile + +class AsyncAPIBrowserUrlProvider: WebBrowserUrlProvider() { + + private val asyncApiSchemaDetector = AsyncAPISchemaDetector() + private val staticServer = StaticServer() + + override fun canHandleElement(request: OpenInBrowserRequest): Boolean { + if (request.file is JsonFile || request.file is YAMLFile) { + if (asyncApiSchemaDetector.isAsyncAPISchema(request.file as? PsiFile)) { + return true + } + } + + return false + } + + override fun getUrl(request: OpenInBrowserRequest, file: VirtualFile): Url? { + return staticServer.getUrl(request, file) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/StaticServer.kt b/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/StaticServer.kt new file mode 100644 index 0000000..4d3a368 --- /dev/null +++ b/src/main/kotlin/com/asyncapi/plugin/idea/extensions/web/StaticServer.kt @@ -0,0 +1,144 @@ +package com.asyncapi.plugin.idea.extensions.web + +import com.asyncapi.plugin.idea._core.SchemaHtmlRenderer +import com.intellij.ide.browsers.OpenInBrowserRequest +import com.intellij.json.JsonFileType +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.Urls +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.* +import org.jetbrains.ide.BuiltInServerManager +import org.jetbrains.ide.HttpRequestHandler +import org.jetbrains.io.send +import org.jetbrains.yaml.YAMLFileType +import java.io.File +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.util.* + +class StaticServer : HttpRequestHandler() { + + private val schemaHtmlRenderer = SchemaHtmlRenderer() + + fun getUrl(request: OpenInBrowserRequest, file: VirtualFile): Url? { + val port = BuiltInServerManager.getInstance().port + val schemaUrl = URLEncoder.encode(file.path, StandardCharsets.UTF_8.toString()) + val projectUrl = URLEncoder.encode(request.project.presentableUrl, StandardCharsets.UTF_8.toString()) + val projectName = URLEncoder.encode(request.project.name, StandardCharsets.UTF_8.toString()) + val address = "http://localhost:$port/asyncapi/render" + + "?schemaUrl=$schemaUrl" + + "&projectUrl=$projectUrl" + + "&projectName=$projectName" + + "&_ij_reload=RELOAD_ON_SAVE" + + val url = Urls.parseEncoded(address) +// val url = Urls.parseEncoded(getStaticUrl()) + return if (request.isAppendAccessToken) { + BuiltInServerManager.getInstance().addAuthToken(Objects.requireNonNull(url)) + } else { + url + } + } + + override fun isAccessible(request: HttpRequest): Boolean { + return request.uri().startsWith("/asyncapi") && super.isAccessible(request) + } + + override fun isSupported(request: FullHttpRequest): Boolean { + return request.uri().startsWith("/asyncapi") && super.isAccessible(request) + } + + override fun process( + urlDecoder: QueryStringDecoder, + request: FullHttpRequest, + context: ChannelHandlerContext + ): Boolean { + val htmlPage = urlDecoder.path().startsWith("/asyncapi/render") + val reference = urlDecoder.path().startsWith("/asyncapi/resources") && urlDecoder.parameters().contains("referenceUrl") + val schema = urlDecoder.path().startsWith("/asyncapi/resources") && urlDecoder.parameters().contains("schemaUrl") + + return if (htmlPage) { + val schemaUrl = if (urlDecoder.parameters().contains("schemaUrl")) { + urlDecoder.parameters()["schemaUrl"]?.get(0) + } else { + null + } + + val html = schemaHtmlRenderer.render(schemaUrl).toByteArray(StandardCharsets.UTF_8) + + sendResponse(html, request, context, "text/html") + true + } else if (reference) { + val referenceUrl = if (urlDecoder.parameters().contains("referenceUrl")) { + urlDecoder.parameters()["referenceUrl"]?.get(0) + } else { + null + } + + referenceUrl ?: return false + val requestedFile = resolveResource(referenceUrl) + requestedFile ?: return false + + sendResponse(requestedFile.second, request, context, requestedFile.first) + true + } else if (schema) { + val schemaUrl = if (urlDecoder.parameters().contains("schemaUrl")) { + urlDecoder.parameters()["schemaUrl"]?.get(0) + } else { + null + } + + schemaUrl ?: return false + val requestedFile = resolveResource(schemaUrl) + requestedFile ?: return false + + sendResponse(requestedFile.second, request, context, requestedFile.first) + true + } else { + false + } + } + + private fun sendResponse(content: ByteArray, + request: FullHttpRequest, + context: ChannelHandlerContext, + contentType: String + ) { + val response: FullHttpResponse = DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(content) + ) + response.headers()[HttpHeaderNames.CONTENT_TYPE] = "$contentType; charset=UTF-8" + response.headers()[HttpHeaderNames.CACHE_CONTROL] = "max-age=5, private, must-revalidate" + response.headers()["Referrer-Policy"] = "no-referrer" + response.send(context.channel(), request) + } + + private fun resolveResource(resourceUrl: String): Pair? { + val requestedFile = File(resourceUrl) + if (!requestedFile.exists()) { + return null + } + + val referenceVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(requestedFile) + referenceVirtualFile ?: return null + + if (referenceVirtualFile.fileType !is YAMLFileType && referenceVirtualFile.fileType !is JsonFileType) { + return null + } + + val isJson = referenceVirtualFile.fileType is JsonFileType + val contentType = if (isJson) { + "application/json" + } else { + "application/x-yaml" + } + + return Pair(contentType, requestedFile.readBytes()) + } + +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 35376d4..9248db4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -42,6 +42,10 @@ + + + + diff --git a/src/main/resources/ui/index.html b/src/main/resources/ui/index.html new file mode 100644 index 0000000..6898e17 --- /dev/null +++ b/src/main/resources/ui/index.html @@ -0,0 +1,24 @@ + + + + + + +
+ + + + + \ No newline at end of file