Skip to content

Commit e9f5aed

Browse files
author
ADFA
committed
Daily merge from stage to main
2 parents 4705cb7 + ada6cd5 commit e9f5aed

4 files changed

Lines changed: 143 additions & 91 deletions

File tree

app/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,12 @@ dependencies {
370370
implementation(libs.google.genai)
371371
implementation(project(":llama-api"))
372372
coreLibraryDesugaring(libs.desugar.jdk.libs.v215)
373+
374+
// Pebble template engine
375+
implementation("io.pebbletemplates:pebble:4.1.1")
376+
377+
// Jackson JSON parsing dependency
378+
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
373379
}
374380

375381
tasks.register("downloadDocDb") {

app/src/main/java/com/itsaky/androidide/localWebServer/WebServer.kt

Lines changed: 132 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import java.io.ByteArrayOutputStream
88
import java.io.File
99
import java.io.InputStream
1010
import java.io.PrintWriter
11+
import java.io.StringWriter
1112
import android.net.TrafficStats
1213
import java.net.InetSocketAddress
1314
import java.net.ServerSocket
@@ -18,6 +19,14 @@ import java.text.SimpleDateFormat
1819
import java.util.Locale
1920
import java.util.concurrent.TimeUnit
2021
import java.util.concurrent.atomic.AtomicReference
22+
import io.pebbletemplates.pebble.PebbleEngine
23+
import io.pebbletemplates.pebble.loader.StringLoader
24+
import java.util.concurrent.ConcurrentHashMap
25+
import io.pebbletemplates.pebble.template.PebbleTemplate
26+
import com.fasterxml.jackson.core.type.TypeReference;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
29+
2130
data class ServerConfig(
2231
val port: Int = 6174,
2332
val databasePath: String,
@@ -52,6 +61,10 @@ class WebServer(private val config: ServerConfig) {
5261
private val experimentsEnabled : Boolean = File(config.experimentsEnablePath).exists() // Frozen at startup. Restart server if needed.
5362
private val encodingHeader : String = "Accept-Encoding"
5463
private val brotliCompression : String = "br"
64+
private val pebbleEngine = PebbleEngine.Builder().loader(StringLoader()).build()
65+
private val templateCache = ConcurrentHashMap<Int, PebbleTemplate>()
66+
67+
private val contentChunkSize = 1024 * 1024
5568

5669

5770
//function to obtain the last modified date of a documentation.db database
@@ -111,6 +124,7 @@ FROM LastChange
111124
}
112125

113126
fun start() {
127+
// Hal Eisen: Required to fix StrictMode.VmPolicy.Builder.detectUntaggedSockets()
114128
TrafficStats.setThreadStatsTag(0xC0DE)
115129
try {
116130
log.info(
@@ -321,118 +335,147 @@ clientSocket and the catch block logic are updated accordingly.
321335
}
322336
}
323337

338+
// Database fetch
324339
val query = """
325-
SELECT C.content, CT.value, CT.compression
326-
FROM Content C, ContentTypes CT
327-
WHERE C.contentTypeID = CT.id
328-
AND C.path = ?
340+
SELECT C.content, CT.value, CT.compression, C.templateId
341+
FROM Content C, ContentTypes CT
342+
WHERE C.contentTypeID = CT.id
343+
AND C.path = ?
329344
"""
330345
val cursor = database.rawQuery(query, arrayOf(path))
331-
val rowCount = cursor.count
332-
333-
if (debugEnabled) log.debug("Database fetch for path='{}' returned {} rows.", path, rowCount)
334-
335-
var dbContent : ByteArray
336-
var dbMimeType : String
337-
var compression : String
338346

347+
// Process database fetch
339348
try {
340-
if (rowCount != 1) {
341-
return when (rowCount) {
342-
0 -> sendError(writer, output, 404, "Not Found", "Path requested: '$path'.")
343-
else -> sendError(
344-
writer,
345-
output,
346-
500,
347-
"Internal Server Error 2",
348-
"Corrupt database - multiple records found when unique record expected, Path requested: '$path'."
349-
)
350-
}
349+
if (cursor.count != 1) {
350+
return if (cursor.count == 0) sendError(writer, output, 404, "Not Found")
351+
else sendError(writer, output, 500, "Corrupt database - multiple records found when unique record expected, Path requested: '$path'.")
351352
}
352353

353354
cursor.moveToFirst()
354-
dbContent = cursor.getBlob(0)
355-
dbMimeType = cursor.getString(1)
356-
compression = cursor.getString(2)
355+
var dbContent = cursor.getBlob(0)
356+
val dbMimeType = cursor.getString(1)
357+
var compression = cursor.getString(2)
358+
val templateId = cursor.getInt(3)
359+
360+
// Fragment handling for large content (> 1MB)
361+
if (dbContent.size == contentChunkSize) {
362+
val query2 = "SELECT content FROM Content WHERE path = ? AND languageId = 1"
363+
var fragmentNumber = 1
364+
val combined = ByteArrayOutputStream().apply {write(dbContent)}
365+
var dbContent2 = dbContent
366+
while (dbContent2.size == contentChunkSize) {
367+
val path2 = "$path-$fragmentNumber"
368+
val cursor2 = database.rawQuery(query2, arrayOf(path2))
369+
try {
370+
if (cursor2.moveToFirst()) {
371+
dbContent2 = cursor2.getBlob(0)
372+
combined.write(dbContent2)
373+
fragmentNumber++
374+
} else break
375+
} finally { cursor2.close() }
376+
}
377+
dbContent = combined.toByteArray()
378+
}
357379

358-
if (debugEnabled) log.debug("len(content)={}, MIME type={}, compression={}.", dbContent.size, dbMimeType, compression)
380+
// If a document is stored in brotli form and the client doesn't support that encoding
381+
// decompress and send that to the client.
382+
// Pebble templates have to be in string form so the retrieved database content may need to be
383+
// decompressed.
384+
if (compression == "brotli" && (!brotliSupported || templateId > 0)) {
385+
dbContent = BrotliInputStream(ByteArrayInputStream(dbContent)).use { it.readBytes() }
386+
compression = "none"
387+
} else if (compression == "brotli") {
388+
compression = "br"
389+
}
359390

391+
// If the file is associated with a template, instantiate that template and send the result to the client
392+
if (templateId > 0) {
393+
dbContent = instantiatePebbleTemplate(templateId, dbContent, path, dbMimeType, compression)
394+
}
395+
396+
writer.println("HTTP/1.1 200 OK")
397+
writer.println("Content-Type: $dbMimeType")
398+
writer.println("Content-Length: ${dbContent.size}")
399+
if (compression != "none") writer.println("Content-Encoding: $compression")
400+
writer.println("Connection: close")
401+
writer.println()
402+
writer.flush()
403+
output.write(dbContent)
404+
output.flush()
405+
} catch (e: Exception) {
406+
log.error("Error processing request: {}", e.message)
407+
sendError(writer, output, 500, "Internal Server Error", e.message ?: "")
360408
} finally {
361409
cursor.close()
362410
}
411+
}
363412

364-
if (dbContent.size == 1024 * 1024) { // Could use fragmentation to satisfy range requests but only for uncompressed content.
365-
val query2 = """
366-
SELECT content
367-
FROM Content
368-
WHERE path = ?
369-
AND languageId = 1
370-
"""
371-
var fragmentNumber = 1
372-
var dbContent2 = dbContent
373-
374-
while (dbContent2.size == 1024 * 1024) {
375-
val path2 = "$path-$fragmentNumber"
376-
if (debugEnabled) log.debug("DB item > 1 MB. fragment#{} path2='{}'.", fragmentNumber, path2)
377-
378-
val cursor2 = database.rawQuery(query2, arrayOf(path2))
379-
try {
380-
if (cursor2.moveToFirst()) {
381-
dbContent2 = cursor2.getBlob(0)
413+
private fun instantiatePebbleTemplate(templateId: Int, dbContent: ByteArray, path: String, dbMimeType: String, compression: String): ByteArray {
414+
if (debugEnabled) log.debug("Processing template for templateId={}", templateId)
415+
416+
// 1. Get or Compile Template from Cache
417+
val compiledTemplate = templateCache.getOrPut(templateId) {
418+
if (debugEnabled) log.debug(
419+
"Template cache miss for ID {}, path {}, MIME type {}, compression {}}",
420+
templateId,
421+
path,
422+
dbMimeType,
423+
compression
424+
)
382425

383-
} else {
384-
log.error("No fragment found for path '{}'.", path2)
385-
break
426+
val tQuery = "SELECT content FROM Templates WHERE id = ?"
427+
val tCursor = database.rawQuery(tQuery, arrayOf(templateId.toString()))
428+
tCursor.use { cursor ->
429+
when {
430+
cursor.count == 0 -> {
431+
log.debug(
432+
"Template not found, for ID {}, path {}, MIME type {}, compression {}",
433+
templateId,
434+
path,
435+
dbMimeType,
436+
compression
437+
)
438+
throw Exception("Template ID $templateId not found in the database")
439+
}
440+
cursor.count > 1 -> {
441+
log.debug(
442+
"More than one template found, for ID {}, path {}, MIME type {}, compression {}",
443+
templateId,
444+
path,
445+
dbMimeType,
446+
compression
447+
)
448+
throw Exception("Template ID $templateId is shared by more than one template")
449+
}
450+
!cursor.moveToFirst() -> {
451+
log.debug(
452+
"Template not found, for ID {}, path {}, MIME type {}, compression {}",
453+
templateId,
454+
path,
455+
dbMimeType,
456+
compression
457+
)
458+
throw Exception("Template ID $templateId not found in database.")
459+
}
460+
else -> {
461+
val templateBlob = cursor.getBlob(0)
462+
pebbleEngine.getTemplate(templateBlob.toString(Charsets.UTF_8))
386463
}
387-
388-
} finally {
389-
cursor2.close()
390-
}
391-
392-
dbContent += dbContent2 // TODO: Is there a faster way to do this? Is data being copied multiple times? --D.S., 22-Jul-2025
393-
fragmentNumber += 1
394-
if (debugEnabled) log.debug("Fragment size={}, dbContent.length={}.", dbContent2.size, dbContent.size)
395-
}
396-
}
397-
398-
// If the Accept-Encoding header contains "br", the client can handle
399-
// Brotli. Send Brotli data as-is, without decompressing it here.
400-
// If the client can't handle Brotli, and the content is Brotli-
401-
// compressed, decompress the content here.
402-
403-
if (compression == "brotli") {
404-
if (brotliSupported) {
405-
compression = "br"
406-
407-
} else {
408-
try {
409-
if (debugEnabled) log.debug("Brotli content but client doesn't support Brotli. Decoding locally.")
410-
dbContent = BrotliInputStream(ByteArrayInputStream(dbContent)).use { it.readBytes() }
411-
compression = "none"
412-
413-
} catch (e: Exception) {
414-
log.error("Error decompressing Brotli content: {}", e.message)
415-
return sendError(writer, output, 500, "Internal Server Error 3")
416464
}
417465
}
418466
}
419467

420-
//send our response
421-
writer.println("HTTP/1.1 200 OK")
422-
writer.println("Content-Type: $dbMimeType")
423-
writer.println("Content-Length: ${dbContent.size}")
424-
425-
if (compression != "none") {
426-
writer.println("Content-Encoding: $compression")
427-
}
468+
// Load JSON data into a template context Map<> for instantiation
469+
val mapper = ObjectMapper()
470+
val context: Map<String, Any> = mapper.readValue(dbContent.toString(Charsets.UTF_8), object : TypeReference<Map<String, Any>>() {})
428471

429-
writer.println("Connection: close")
430-
writer.println()
431-
writer.flush()
432-
output.write(dbContent)
433-
output.flush()
472+
// Evaluate template with loaded data and return the output
473+
val sw = StringWriter()
474+
compiledTemplate.evaluate(sw, context)
475+
return sw.toString().toByteArray()
434476
}
435477

478+
436479
private fun handleDbEndpoint(writer: PrintWriter, output: java.io.OutputStream) {
437480
if (debugEnabled) log.debug("Entering handleDbEndpoint().")
438481

plugin-api/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ android {
2121
kotlin {
2222
compilerOptions {
2323
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
24+
// Pin to match the on-device Kotlin compiler
25+
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)
26+
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)
2427
}
2528
}
2629

subprojects/kotlin-analysis-api/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212

1313
val ktAndroidRepo = "https://github.com/appdevforall/kotlin-android"
1414
val ktAndroidVersion = "2.3.255"
15-
val ktAndroidTag = "v${ktAndroidVersion}-853aa4b"
15+
val ktAndroidTag = "v${ktAndroidVersion}-172a7e7"
1616
val ktAndroidJarName = "analysis-api-standalone-embeddable-for-ide-${ktAndroidVersion}-SNAPSHOT.jar"
1717

1818
externalAssets {
@@ -21,7 +21,7 @@ externalAssets {
2121
source =
2222
AssetSource.External(
2323
url = uri("$ktAndroidRepo/releases/download/$ktAndroidTag/$ktAndroidJarName"),
24-
sha256Checksum = "f4f3fec4b4b701620cc2727ce533a873e5ea10f3966bde6e2abdb40ce1e6e875",
24+
sha256Checksum = "ee52466a893ed7261fb542a259cb469227aa8059cf7a36b5d1b41897a7e5bb08",
2525
)
2626
}
2727
}

0 commit comments

Comments
 (0)