├── .gitignore ├── server ├── src │ ├── main │ │ ├── resources │ │ │ ├── assets │ │ │ │ ├── icons │ │ │ │ │ ├── ide │ │ │ │ │ ├── nodes │ │ │ │ │ ├── actions │ │ │ │ │ ├── fileTypes │ │ │ │ │ ├── general │ │ │ │ │ ├── history_dark.svg │ │ │ │ │ ├── documentation.svg │ │ │ │ │ └── user.svg │ │ │ │ ├── robots.txt │ │ │ │ ├── icons.css │ │ │ │ ├── references.css │ │ │ │ ├── search.css │ │ │ │ └── theme-default.css │ │ │ └── at │ │ │ │ └── yawk │ │ │ │ └── javabrowser │ │ │ │ └── server │ │ │ │ ├── view │ │ │ │ ├── diffIcon.ftl │ │ │ │ ├── declarationNodeView.ftl │ │ │ │ ├── alternatives.ftl │ │ │ │ ├── diffStats.ftl │ │ │ │ ├── fullTextSearchForm.ftl │ │ │ │ ├── directoryView.ftl │ │ │ │ ├── search-dialog.ftl │ │ │ │ ├── directory.ftl │ │ │ │ ├── fullTextSearchResultView.ftl │ │ │ │ ├── source-file.ftl │ │ │ │ ├── index.ftl │ │ │ │ ├── metadata.ftl │ │ │ │ ├── page.ftl │ │ │ │ └── leafArtifact.ftl │ │ │ │ └── InitInteractiveSchema.sql │ │ └── kotlin │ │ │ └── at │ │ │ └── yawk │ │ │ └── javabrowser │ │ │ └── server │ │ │ ├── view │ │ │ ├── View.kt │ │ │ ├── IndexView.kt │ │ │ ├── DeclarationNodeView.kt │ │ │ ├── SiteStatistics.kt │ │ │ ├── DirectoryView.kt │ │ │ ├── LeafArtifactView.kt │ │ │ ├── Alternative.kt │ │ │ ├── FullTextSearchResultView.kt │ │ │ ├── ReferenceDetailView.kt │ │ │ ├── DeclarationNode.kt │ │ │ └── SourceFileView.kt │ │ │ ├── HttpException.kt │ │ │ ├── LongRunningDbi.kt │ │ │ ├── sqlUtil.kt │ │ │ ├── Config.kt │ │ │ ├── DependencyDao.kt │ │ │ ├── cborMapper.kt │ │ │ ├── PrepareStatementImmediately.kt │ │ │ ├── ParsedPath.kt │ │ │ ├── Locations.kt │ │ │ ├── Debouncer.kt │ │ │ ├── crawlers.kt │ │ │ ├── TreeIterator.kt │ │ │ ├── VersionComparator.kt │ │ │ ├── typesearch │ │ │ └── BindingTokenizer.kt │ │ │ ├── ConservativeLoopBlock.kt │ │ │ ├── ArtifactMetadataCache.kt │ │ │ ├── ServerSourceFile.kt │ │ │ ├── AliasIndex.kt │ │ │ ├── ArtifactIndex.kt │ │ │ ├── BindingResolver.kt │ │ │ ├── SiteStatisticsService.kt │ │ │ ├── ArtifactUpdater.kt │ │ │ ├── Escaper.kt │ │ │ ├── Ftl.kt │ │ │ ├── artifact │ │ │ └── ArtifactNode.kt │ │ │ └── diff.kt │ └── test │ │ └── kotlin │ │ └── at │ │ └── yawk │ │ └── javabrowser │ │ └── server │ │ ├── typesearch │ │ ├── BindingTokenizerTest.kt │ │ ├── IndexChunkSizeMemoryBenchmark.kt │ │ ├── IndexAutomatonTest.kt │ │ └── IndexChunkSizeRuntimeBenchmark.kt │ │ ├── artifact │ │ └── ArtifactNodeTest.kt │ │ ├── integrationTestUtil.kt │ │ ├── DiffTest.kt │ │ ├── EscaperTest.kt │ │ ├── ReferenceResourceIntegrationTest.kt │ │ └── FullTextSearchResourceIntegrationTest.kt └── config.yml ├── .gitmodules ├── README.md ├── generator └── src │ ├── main │ ├── kotlin │ │ ├── org │ │ │ ├── objectweb │ │ │ │ └── asm │ │ │ │ │ └── attributes.kt │ │ │ └── eclipse │ │ │ │ └── jdt │ │ │ │ └── internal │ │ │ │ └── compiler │ │ │ │ └── parser │ │ │ │ └── PrepareMonkeyPatch.kt │ │ └── at │ │ │ └── yawk │ │ │ └── javabrowser │ │ │ └── generator │ │ │ ├── db │ │ │ ├── TransactionProvider.kt │ │ │ ├── LimitedTransactionProvider.kt │ │ │ ├── InPlaceUpdateStrategy.kt │ │ │ ├── Transaction.kt │ │ │ ├── AsyncTransaction.kt │ │ │ ├── ActorAsyncTransactionProvider.kt │ │ │ ├── FullUpdateStrategy.kt │ │ │ └── UpdateStrategy.kt │ │ │ ├── SourceSetConfig.kt │ │ │ ├── bytecode │ │ │ ├── JdtInformation.kt │ │ │ ├── descriptions.kt │ │ │ ├── BytecodeBindings.kt │ │ │ ├── typeReferences.kt │ │ │ ├── fields.kt │ │ │ ├── Flag.kt │ │ │ └── bindings.kt │ │ │ ├── CheckLogoUris.kt │ │ │ ├── LineNumberTable.kt │ │ │ ├── CoroutineCloseable.kt │ │ │ ├── source │ │ │ ├── Printer.kt │ │ │ ├── ModuleFileParser.kt │ │ │ └── Bindings.kt │ │ │ ├── GeneratorSchema.kt │ │ │ ├── ListUpdates.kt │ │ │ ├── work │ │ │ ├── PrepareArtifactWorker.kt │ │ │ └── TempDirProvider.kt │ │ │ ├── ListMissingDependencies.kt │ │ │ ├── ArtifactConfig.kt │ │ │ ├── GeneratorConfigTypeModule.kt │ │ │ ├── KeywordHandler.kt │ │ │ ├── MultiSemaphore.kt │ │ │ ├── GeneratorSourceFile.kt │ │ │ ├── JavadocRenderVisitor.kt │ │ │ └── ResumingUrlInputStream.kt │ └── resources │ │ └── at │ │ └── yawk │ │ └── javabrowser │ │ └── generator │ │ └── DataIndex.sql │ └── test │ └── kotlin │ └── at │ └── yawk │ └── javabrowser │ └── generator │ ├── work │ ├── TimeoutDumpListener.kt │ ├── TempDirProviderTest.kt │ ├── PrepareAndroidWorkerTest.kt │ └── PrepareGraalWorkerTest.kt │ ├── MavenTest.kt │ ├── source │ └── ModuleFileParserTest.kt │ ├── LineNumberTableTest.kt │ ├── bytecode │ ├── SignatureTest.kt │ ├── testCodegen.kt │ └── ModulePrinterTest.kt │ ├── MultiSemaphoreTest.kt │ └── db │ ├── TestTransaction.kt │ └── ActorAsyncTransactionProviderTest.kt ├── .github └── workflows │ └── maven.yml ├── shared └── src │ ├── main │ ├── kotlin │ │ └── at │ │ │ └── yawk │ │ │ └── javabrowser │ │ │ ├── PositionedAnnotation.kt │ │ │ ├── BindingId.kt │ │ │ ├── Realm.kt │ │ │ ├── DbConfig.kt │ │ │ ├── ArtifactMetadata.kt │ │ │ └── TsQuery.kt │ └── resources │ │ └── logback.xml │ └── test │ └── kotlin │ └── at │ └── yawk │ └── javabrowser │ ├── SourceAnnotationTest.kt │ ├── CborMapperTest.kt │ └── IntRangeSetTest.kt ├── benchmark-index-data-memory.tsv └── benchmark-index-plot.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | target/ 4 | in/ 5 | html/types/ -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/ide: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/ide -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/nodes: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/nodes -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/actions: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/actions -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/fileTypes: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/fileTypes -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/general: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/general -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/history_dark.svg: -------------------------------------------------------------------------------- 1 | ../../../../../../intellij-community/platform/icons/src/vcs/history_dark.svg -------------------------------------------------------------------------------- /server/src/main/resources/assets/robots.txt: -------------------------------------------------------------------------------- 1 | user-agent: * 2 | disallow: /api 3 | disallow: /references 4 | disallow: /declarationTree 5 | disallow: /fts 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "intellij-community"] 2 | path = intellij-community 3 | url = https://github.com/JetBrains/intellij-community.git 4 | shallow = true 5 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/View.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | /** 4 | * @author yawkat 5 | */ 6 | abstract class View(val templateFile: String) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code.yawk.at [![Build Status](https://ci.yawk.at/job/java-browser/badge/icon)](https://ci.yawk.at/job/java-browser/) 2 | 3 | Online browser for java open-source projects. Aims to replace the now-defunct grepcode. -------------------------------------------------------------------------------- /server/config.yml: -------------------------------------------------------------------------------- 1 | database: 2 | user: java-browser 3 | password: password 4 | url: jdbc:postgresql://192.168.2.11/java-browser 5 | bindAddress: 0.0.0.0 6 | bindPort: 8080 7 | typeIndexChunkSize: 512 8 | typeIndexDirectory: /var/tmp -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/HttpException.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | /** 4 | * @author yawkat 5 | */ 6 | class HttpException( 7 | val status: Int, 8 | message: String 9 | ) : Exception(message) -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/LongRunningDbi.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import com.google.inject.BindingAnnotation 4 | 5 | @Retention(AnnotationRetention.RUNTIME) 6 | @BindingAnnotation 7 | annotation class LongRunningDbi -------------------------------------------------------------------------------- /generator/src/main/kotlin/org/objectweb/asm/attributes.kt: -------------------------------------------------------------------------------- 1 | package org.objectweb.asm 2 | 3 | import java.nio.ByteBuffer 4 | 5 | val Attribute.content: ByteBuffer 6 | get() { 7 | val vector = write(null, null, 0, 0, 0) 8 | return ByteBuffer.wrap(vector.data, 0, vector.length) 9 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/TransactionProvider.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | interface TransactionProvider { 4 | suspend fun claimArtifactId(): Long 5 | 6 | suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) 7 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/sqlUtil.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.intellij.lang.annotations.Language 4 | 5 | @Language("sql", prefix = "select ") 6 | fun escapeLike(@Language("sql", prefix = "select ") parameter: String) = 7 | "regexp_replace($parameter, '([\\\\%_])', '\\\\\\1', 'g')" 8 | -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/documentation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/IndexView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.server.ParsedPath 4 | 5 | /** 6 | * @author yawkat 7 | */ 8 | @Suppress("unused") 9 | class IndexView( 10 | val path: ParsedPath, 11 | val siteStatistics: SiteStatistics 12 | ) : View("index.ftl") 13 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/DeclarationNodeView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | /** 4 | * @author yawkat 5 | */ 6 | data class DeclarationNodeView( 7 | val children: Iterator, 8 | val parentBinding: String?, 9 | val diffArtifactId: String? 10 | ) : View("declarationNodeView.ftl") -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | - name: Build with Maven 17 | run: mvn -B package --file pom.xml 18 | -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/PositionedAnnotation.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | 5 | /** 6 | * @author yawkat 7 | */ 8 | data class PositionedAnnotation( 9 | val start: Int, 10 | val length: Int, 11 | val annotation: SourceAnnotation 12 | ) { 13 | @get:JsonIgnore 14 | val end: Int 15 | get() = start + length 16 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/Config.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.DbConfig 4 | import java.nio.file.Path 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | data class Config( 10 | val database: DbConfig, 11 | val bindAddress: String, 12 | val bindPort: Int, 13 | val typeIndexChunkSize: Int = 32, 14 | val typeIndexDirectory: Path? = null 15 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/DependencyDao.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.jdbi.v3.sqlobject.customizer.Bind 4 | import org.jdbi.v3.sqlobject.statement.SqlQuery 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | interface DependencyDao { 10 | @SqlQuery("select to_artifact from dependency where from_artifact = :artifactId") 11 | fun getDependencies(@Bind("artifactId") artifactId: Long): List 12 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/cborMapper.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.CompressedFactory 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.KotlinModule 6 | import com.fasterxml.jackson.module.kotlin.SingletonSupport 7 | 8 | internal val cborMapper = ObjectMapper(CompressedFactory()).registerModule(KotlinModule(singletonSupport = SingletonSupport.CANONICALIZE)) -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/SiteStatistics.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | /** 4 | * @author yawkat 5 | */ 6 | data class SiteStatistics( 7 | val artifactCount: Long, 8 | val sourceFileCount: Long, 9 | val classCount: Long, 10 | val bindingCount: Long, 11 | val referenceCount: Long, 12 | val lexemeCountNoSymbols: Long, 13 | val lexemeCountWithSymbols: Long 14 | ) -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/diffIcon.ftl: -------------------------------------------------------------------------------- 1 | <#macro diffIcon node> 2 | <#if node.diffResult??> 3 | <#t> 4 | <#if node.diffResult == "INSERTION"> 5 | +<#t> 6 | <#elseif node.diffResult == "DELETION"> 7 | -<#t> 8 | <#elseif node.diffResult == "CHANGED_INTERNALLY"> 9 | ~<#t> 10 | 11 | <#t> 12 | 13 | -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/declarationNodeView.ftl: -------------------------------------------------------------------------------- 1 | <#import "declarationNode.ftl" as declarationNode> 2 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.DeclarationNodeView" --> 3 | 4 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/SourceSetConfig.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import java.nio.file.Path 4 | 5 | data class SourceSetConfig( 6 | val debugTag: String, 7 | val sourceRoot: Path, 8 | val dependencies: List, 9 | val includeRunningVmBootclasspath: Boolean = true, 10 | val pathPrefix: String = "", 11 | val outputClassesTo: Path? = null, 12 | 13 | val quirkIsJavaBase: Boolean = false 14 | ) 15 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/JdtInformation.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import org.objectweb.asm.Type 4 | 5 | internal class JdtInformation { 6 | val missingTypes = mutableListOf() 7 | 8 | fun isMissing(type: Type): Boolean = when (type.sort) { 9 | Type.ARRAY -> isMissing(type.elementType) 10 | Type.METHOD -> type.argumentTypes.any { isMissing(it) } || isMissing(type.returnType) 11 | else -> type in missingTypes 12 | } 13 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/alternatives.ftl: -------------------------------------------------------------------------------- 1 | <#macro alternatives alternatives> 2 | <#-- @ftlvariable name="alternatives" type="java.util.List" --> 3 | 6 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/BindingId.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonValue 5 | 6 | @JvmInline 7 | @Suppress("EXPERIMENTAL_FEATURE_WARNING") 8 | value class BindingId @JsonCreator constructor(@get:JsonValue val hash: Long) { 9 | companion object { 10 | /** 11 | * The top-level "" package has this fixed binding id 12 | */ 13 | val TOP_LEVEL_PACKAGE = BindingId(-1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/diffStats.ftl: -------------------------------------------------------------------------------- 1 | <#macro diffStats newPath oldPath> 2 | <#-- @ftlvariable name="newPath" type="at.yawk.javabrowser.server.ParsedPath" --> 3 | <#-- @ftlvariable name="oldPath" type="at.yawk.javabrowser.server.ParsedPath" --> 4 | Showing changes in 5 | ${newPath.artifact.stringId}<#if newPath.sourceFilePath??>/${newPath.sourceFilePath} (new version) from 6 | ${oldPath.artifact.stringId}<#if oldPath.sourceFilePath??>/${oldPath.sourceFilePath} (old version). 7 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/CheckLogoUris.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import java.net.URL 4 | import java.nio.file.Paths 5 | 6 | fun main(args: Array) { 7 | val cfg = Config.fromFile(Paths.get(args[0])) 8 | val logoUrls = cfg.artifacts.mapNotNull { it.metadata?.logoUrl }.toSet() 9 | for (logoUrl in logoUrls) { 10 | println("Checking $logoUrl") 11 | URL(logoUrl).openConnection() 12 | .also { it.setRequestProperty("User-Agent", "code.yawk.at image cache fetcher") } 13 | .getInputStream() 14 | .use { it.readBytes() } 15 | } 16 | } -------------------------------------------------------------------------------- /server/src/main/resources/assets/icons.css: -------------------------------------------------------------------------------- 1 | .ij { 2 | height: 1.1em; 3 | width: 1.1em; 4 | display: inline-block; 5 | background-size: contain; 6 | vertical-align: middle; 7 | } 8 | .ij-home { 9 | background-image: url('/assets/icons/nodes/homeFolder_dark.svg'); 10 | } 11 | .ij-search { 12 | background-image: url('/assets/icons/actions/find_dark.svg'); 13 | } 14 | .ij-history { 15 | background-image: url('/assets/icons/history_dark.svg'); 16 | } 17 | .ij-user { 18 | background-image: url('/assets/icons/user.svg'); 19 | } 20 | .ij-documentation { 21 | background-image: url('/assets/icons/documentation.svg'); 22 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/PrepareStatementImmediately.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.jdbi.v3.core.statement.StatementContext 4 | import org.jdbi.v3.core.statement.StatementCustomizer 5 | import org.postgresql.PGStatement 6 | import java.sql.PreparedStatement 7 | 8 | /** 9 | * @author yawkat 10 | */ 11 | object PrepareStatementImmediately : StatementCustomizer { 12 | override fun beforeExecution(stmt: PreparedStatement, ctx: StatementContext) { 13 | stmt.unwrap(PGStatement::class.java).prepareThreshold = -1 14 | } 15 | 16 | override fun afterExecution(stmt: PreparedStatement, ctx: StatementContext) { 17 | } 18 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/LineNumberTable.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import org.eclipse.collections.impl.factory.primitive.IntLists 4 | 5 | /** 6 | * @author yawkat 7 | */ 8 | class LineNumberTable(text: CharSequence) { 9 | private val lineOffsetTable = IntLists.mutable.of(0) 10 | 11 | init { 12 | for (i in text.indices) { 13 | if (text[i] == '\n') { 14 | lineOffsetTable.add(i + 1) 15 | } 16 | } 17 | } 18 | 19 | fun lineAt(index: Int): Int { 20 | val res = lineOffsetTable.binarySearch(index) 21 | return if (res < 0) res.inv() else res + 1 22 | } 23 | } -------------------------------------------------------------------------------- /shared/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%25.25logger{25}] [%-5level]: %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/descriptions.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingDecl 4 | import org.objectweb.asm.Type 5 | 6 | val Type.simpleName: String 7 | get() = className.let { it.substring(it.lastIndexOf('.') + 1) } 8 | 9 | internal fun typeDescription(printer: BytecodePrinter, type: Type): BindingDecl.Description.Type = BindingDecl.Description.Type( 10 | kind = BindingDecl.Description.Type.Kind.CLASS, 11 | binding = if (type.sort == Type.OBJECT || type.sort == Type.ARRAY) printer.hashBinding(BytecodeBindings.toStringClass(type)) else null, 12 | simpleName = type.simpleName 13 | ) 14 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/CoroutineCloseable.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | interface CoroutineCloseable { 4 | suspend fun close() 5 | } 6 | 7 | suspend inline fun C.use(task: (C) -> R): R { 8 | var exc: Throwable? = null 9 | try { 10 | return task(this) 11 | } catch (t: Throwable) { 12 | exc = t 13 | throw t 14 | } finally { 15 | if (exc == null) { 16 | close() 17 | } else { 18 | try { 19 | close() 20 | } catch (nested: Throwable) { 21 | exc.addSuppressed(nested) 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/work/TimeoutDumpListener.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.debug.DebugProbes 5 | import org.testng.ITestResult 6 | import org.testng.TestListenerAdapter 7 | import org.testng.internal.thread.ThreadTimeoutException 8 | 9 | @ExperimentalCoroutinesApi 10 | class TimeoutDumpListener : TestListenerAdapter() { 11 | init { 12 | DebugProbes.install() 13 | } 14 | 15 | override fun onTestFailure(result: ITestResult) { 16 | if (result.throwable is ThreadTimeoutException) { 17 | DebugProbes.dumpCoroutines(System.err) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/MavenTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | class MavenTest { 10 | @Test 11 | fun deps() { 12 | val dependencies = MavenDependencyResolver().getMavenDependencies("com.google.guava", "guava", "25.1-jre") 13 | Assert.assertTrue( 14 | dependencies.map { it.coordinate.toCanonicalForm() } 15 | .any { it.matches("com.google.errorprone:.*".toRegex()) } 16 | ) 17 | } 18 | 19 | @Test 20 | fun `central location`() { 21 | MavenDependencyResolver().getMavenDependencies("org.jdbi", "jdbi3-testing", "3.8.2") 22 | } 23 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/fullTextSearchForm.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#macro fullTextSearchForm searchArtifact query=""> 3 | <#-- @ftlvariable name="searchArtifact" type="at.yawk.javabrowser.server.artifact.ArtifactNode" --> 4 |
5 | <#if searchArtifact?has_content> 6 | <#if searchArtifact?has_content> 7 | <#assign text>Search for text in ${searchArtifact.stringId}… 8 | <#else> 9 | <#assign text>Search for text… 10 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/typesearch/BindingTokenizerTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.typesearch 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | class BindingTokenizerTest { 7 | @Test 8 | fun java() { 9 | Assert.assertEquals(BindingTokenizer.Java.tokenize("ConcurrentHashMap"), listOf("concurrent", "hash", "map")) 10 | Assert.assertEquals(BindingTokenizer.Java.tokenize("java.util"), listOf("java.", "util")) 11 | Assert.assertEquals(BindingTokenizer.Java.tokenize("URI"), listOf("uri")) 12 | Assert.assertEquals(BindingTokenizer.Java.tokenize("Jsr320"), listOf("jsr", "320")) 13 | Assert.assertEquals(BindingTokenizer.Java.tokenize("JSR320"), listOf("jsr", "320")) 14 | } 15 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/BytecodeBindings.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import org.objectweb.asm.Type 4 | 5 | object BytecodeBindings { 6 | fun toStringClass(type: Type): String { 7 | require(type.sort == Type.OBJECT || type.sort == Type.ARRAY) 8 | return type.descriptor 9 | } 10 | 11 | fun toStringMethod(declaring: Type, name: String, type: Type): String { 12 | require(type.sort == Type.METHOD) 13 | return declaring.descriptor + '.' + name + ':' + type.descriptor 14 | } 15 | 16 | fun toStringField(declaring: Type, name: String, type: Type): String { 17 | require(type.sort != Type.METHOD) 18 | return declaring.descriptor + '.' + name + ':' + type.descriptor 19 | } 20 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/directoryView.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.DirectoryView" --> 3 | <#import "page.ftl" as page> 4 | <#import "alternatives.ftl" as at> 5 | <#import "directory.ftl" as directory> 6 | <#import "diffStats.ftl" as diffStats> 7 | 8 | <#assign additionalMenu> 9 | <@at.alternatives alternatives/> 10 | 11 | <@page.page title="${newPath.artifact.stringId} : ${newPath.sourceFilePath}" realm=realm newPath=newPath oldPath=oldPath! hasSearch=true additionalTitle=additionalTitle additionalMenu=additionalMenu narrow=true tooltip=true> 12 | <#if oldPath??> 13 | <@diffStats.diffStats newPath oldPath/> 14 | 15 | 16 | <@directory.directory entries/> 17 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/LimitedTransactionProvider.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import kotlinx.coroutines.sync.Semaphore 4 | import kotlinx.coroutines.sync.withPermit 5 | 6 | class LimitedTransactionProvider( 7 | private val delegate: TransactionProvider, 8 | concurrentTransactions: Int = 3 9 | ) : TransactionProvider { 10 | private val semaphore = Semaphore(concurrentTransactions) 11 | 12 | override suspend fun claimArtifactId() = semaphore.withPermit { 13 | delegate.claimArtifactId() 14 | } 15 | 16 | override suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) = 17 | semaphore.withPermit { 18 | delegate.withArtifactTransaction(artifactId, task) 19 | } 20 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/DirectoryView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.Realm 4 | import at.yawk.javabrowser.server.ParsedPath 5 | 6 | data class DirectoryView( 7 | val realm: Realm, 8 | val newPath: ParsedPath.SourceFile, 9 | val oldPath: ParsedPath.SourceFile?, 10 | val entries: List, 11 | val alternatives: List 12 | ): View("directoryView.ftl") { 13 | data class DirectoryEntry( 14 | val name: String, 15 | /** 16 | * Source file path to this binding, including potential diff and realm query parameter. 17 | */ 18 | val fullSourceFilePath: String, 19 | val isDirectory: Boolean, 20 | val diffResult: DeclarationNode.DiffResult? = null 21 | ) 22 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/source/ModuleFileParserTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.source 2 | 3 | import at.yawk.javabrowser.generator.work.TempDirProviderTest 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | import java.nio.file.Files 7 | 8 | class ModuleFileParserTest { 9 | @Test 10 | fun test() { 11 | TempDirProviderTest.withTempDirSync("ModuleFileParserTest") { tmp -> 12 | val moduleInfo = tmp.resolve("module-info.java") 13 | Files.write(moduleInfo, """ 14 | module foo { 15 | requires ab.bar; 16 | } 17 | """.toByteArray()) 18 | val moduleDeclaration = parseModuleFile(moduleInfo) 19 | Assert.assertEquals(getRequiredModules(moduleDeclaration), setOf("ab.bar")) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/LineNumberTableTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | class LineNumberTableTest { 10 | @Test 11 | fun simple() { 12 | val table = LineNumberTable("one\ntwo\nthree\n".trimIndent()) 13 | Assert.assertEquals(table.lineAt(0), 1) 14 | Assert.assertEquals(table.lineAt(1), 1) 15 | Assert.assertEquals(table.lineAt(2), 1) 16 | Assert.assertEquals(table.lineAt(3), 1) 17 | Assert.assertEquals(table.lineAt(4), 2) 18 | Assert.assertEquals(table.lineAt(5), 2) 19 | Assert.assertEquals(table.lineAt(6), 2) 20 | Assert.assertEquals(table.lineAt(7), 2) 21 | Assert.assertEquals(table.lineAt(8), 3) 22 | } 23 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/work/TempDirProviderTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import com.google.common.io.MoreFiles 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | 7 | object TempDirProviderTest : TempDirProvider { 8 | private inline fun impl(debugTag: String, f: (Path) -> R): R { 9 | val tmp = Files.createTempDirectory("code-browser-${debugTag.replace('/', '_')}") 10 | try { 11 | return f(tmp) 12 | } finally { 13 | @Suppress("UnstableApiUsage") 14 | MoreFiles.deleteRecursively(tmp) 15 | } 16 | } 17 | 18 | fun withTempDirSync(debugTag: String, f: (Path) -> R) = impl(debugTag, f) 19 | 20 | override suspend fun withTempDir(debugTag: String, f: suspend (Path) -> R) = impl(debugTag) { f(it) } 21 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ParsedPath.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.Realm 4 | import at.yawk.javabrowser.server.artifact.ArtifactNode 5 | 6 | private fun realmForSourcePath(path: String): Realm? { 7 | if (path.endsWith(".java")) return Realm.SOURCE 8 | if (path.endsWith(".class")) return Realm.BYTECODE 9 | return null 10 | } 11 | 12 | sealed class ParsedPath(val artifact: ArtifactNode) { 13 | /** 14 | * Might also be a directory 15 | */ 16 | class SourceFile(artifact: ArtifactNode, val sourceFilePath: String) : ParsedPath(artifact) { 17 | val realmFromExtension = realmForSourcePath(sourceFilePath) 18 | } 19 | 20 | class LeafArtifact(artifact: ArtifactNode) : ParsedPath(artifact) 21 | class Group(artifact: ArtifactNode) : ParsedPath(artifact) 22 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/artifact/ArtifactNodeTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.artifact 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | class ArtifactNodeTest { 10 | @Test 11 | fun test() { 12 | val root = ArtifactNode.build(listOf(ArtifactNode.Prototype(0, "java/8"))) 13 | Assert.assertEquals(root.children.size, 1) 14 | Assert.assertEquals(root.children["java"]?.children?.size, 1) 15 | Assert.assertEquals(root.children["java"]?.stringId, "java") 16 | Assert.assertEquals(root.children["java"]?.idInParent, "java") 17 | Assert.assertEquals(root.children["java"]?.children?.get("8")?.children?.size, 0) 18 | Assert.assertEquals(root.children["java"]?.children?.get("8")?.stringId, "java/8") 19 | Assert.assertEquals(root.children["java"]?.children?.get("8")?.idInParent, "8") 20 | } 21 | } -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/Realm.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.fasterxml.jackson.annotation.JsonValue 5 | 6 | /** 7 | * A realm describes a global name space for source files, bindings, references and so on. References by default target 8 | * the same realm. The purpose of this is to avoid mixing source references with bytecode references too much. 9 | */ 10 | enum class Realm(@get:JsonValue val id: Byte) { 11 | SOURCE(0), 12 | BYTECODE(1); 13 | 14 | companion object { 15 | private val values = values() 16 | 17 | fun parse(name: String) = when { 18 | name.equals("source", ignoreCase = true) -> SOURCE 19 | name.equals("bytecode", ignoreCase = true) -> BYTECODE 20 | else -> null 21 | } 22 | 23 | @JsonCreator 24 | fun byId(id: Byte) = values.single { it.id == id } 25 | } 26 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/source/Printer.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.source 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.Realm 5 | import at.yawk.javabrowser.Tokenizer 6 | import at.yawk.javabrowser.generator.GeneratorSourceFile 7 | 8 | /** 9 | * @author yawkat 10 | */ 11 | interface Printer { 12 | fun hashBinding(binding: String): BindingId 13 | 14 | suspend fun addSourceFile(path: String, sourceFile: GeneratorSourceFile, tokens: List, realm: Realm) 15 | 16 | class SimplePrinter : Printer { 17 | val sourceFiles: MutableMap = HashMap() 18 | 19 | override fun hashBinding(binding: String) = BindingId(binding.hashCode().toLong()) 20 | 21 | override suspend fun addSourceFile(path: String, sourceFile: GeneratorSourceFile, tokens: List, 22 | realm: Realm) { 23 | sourceFiles[path] = sourceFile 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/search-dialog.ftl: -------------------------------------------------------------------------------- 1 | <#-- @ftlvariable name="artifactId" type="at.yawk.javabrowser.server.artifact.ArtifactNode" --> 2 | <#-- @ftlvariable name="realm" type="at.yawk.javabrowser.Realm" --> 3 |
4 | 19 |
-------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/LeafArtifactView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.Realm 5 | import at.yawk.javabrowser.server.ParsedPath 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | @Suppress("unused") 11 | data class LeafArtifactView( 12 | val path: ParsedPath.LeafArtifact, 13 | val oldPath: ParsedPath.LeafArtifact?, 14 | val artifactMetadata: ArtifactMetadata, 15 | val dependencies: List, 16 | val topLevelPackages: Iterator, 17 | val alternatives: List, 18 | /** keys should be [Realm.name], using [String] keys because that's what freemarker templates can access */ 19 | val topDirectoryEntries: Map> 20 | ) : View("leafArtifact.ftl") { 21 | data class Dependency( 22 | val prefix: String?, 23 | val suffix: String, 24 | val aliasedTo: String? 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/InPlaceUpdateStrategy.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.generator.GeneratorSchema 4 | import org.jdbi.v3.core.Handle 5 | import org.jdbi.v3.core.Jdbi 6 | 7 | class InPlaceUpdateStrategy(dbi: Jdbi) : UpdateStrategy("data", dbi) { 8 | override fun prepare() { 9 | } 10 | 11 | override fun finish(allArtifacts: Collection) { 12 | return dbi.inTransaction { conn: Handle -> 13 | setConnectionSchema(conn) 14 | GeneratorSchema(conn).updateViews(concurrent = true) 15 | } 16 | } 17 | 18 | override suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) { 19 | dbi.inTransactionSuspend { conn -> 20 | setConnectionSchema(conn) 21 | val tx = DirectTransaction(conn) 22 | tx.deleteArtifact(artifactId) 23 | task(tx) 24 | tx.flush() 25 | notifyUpdate(conn, artifactId) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/integrationTestUtil.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.view.View 4 | import org.apache.commons.io.output.NullOutputStream 5 | import org.hamcrest.BaseMatcher 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | import org.jdbi.v3.core.Jdbi 9 | import org.jdbi.v3.sqlobject.SqlObjectPlugin 10 | import org.testng.SkipException 11 | 12 | fun loadIntegrationTestDbi(dataSource: String?): Jdbi { 13 | if (dataSource == null) throw SkipException("Missing dataSource parameter") 14 | return Jdbi.create(dataSource).installPlugin(SqlObjectPlugin()) 15 | } 16 | 17 | inline fun matches(crossinline pred: (T) -> Boolean): Matcher = object : BaseMatcher() { 18 | override fun describeTo(description: Description) { 19 | description.appendText("Matches lambda") 20 | } 21 | 22 | override fun matches(item: Any?) = item is T && pred(item) 23 | } 24 | 25 | fun tryRender(view: View) { 26 | Ftl(ImageCache()).render(view, NullOutputStream(), theme = null, javadocRenderEnabled = false) 27 | } 28 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/Locations.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import java.net.URI 4 | import java.net.URLEncoder 5 | 6 | object Locations { 7 | fun bindingHash(binding: String) = "#${URLEncoder.encode(binding, "UTF-8")}" 8 | 9 | fun location(artifactId: String, sourceFilePath: String, hash: String) = 10 | URI.create(fullSourceFilePath(artifactId, sourceFilePath) + hash)!! 11 | 12 | fun fullSourceFilePath(artifactId: String, sourceFilePath: String) = 13 | "/$artifactId/$sourceFilePath" 14 | 15 | fun toPath(parsedPath: ParsedPath) = when (parsedPath) { 16 | is ParsedPath.SourceFile -> fullSourceFilePath(parsedPath.artifact.stringId, parsedPath.sourceFilePath) 17 | is ParsedPath.Group, is ParsedPath.LeafArtifact -> "/${parsedPath.artifact.stringId}" 18 | } 19 | 20 | /** 21 | * Path to the diff view of two source files. Source file paths *must not* contain binding hashes. 22 | */ 23 | fun diffPath(sourceFileNew: String, sourceFileOld: String) = 24 | "$sourceFileNew?diff=${URLEncoder.encode(sourceFileOld, "UTF-8")}" 25 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/directory.ftl: -------------------------------------------------------------------------------- 1 | <#import "diffIcon.ftl" as diffIcon> 2 | 3 | <#macro directory entries> 4 | <#-- @ftlvariable name="entries" type="java.util.List" --> 5 | 22 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/Debouncer.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import java.util.concurrent.locks.ReentrantLock 4 | 5 | /** 6 | * This class facilitates "debouncing" an operation. 7 | * 8 | * Class users can "request" that a task be run at an indeterminate time in the future, but importantly *after* the 9 | * request is made (so that data changes that caused the request are up-to-date). Should many users request this 10 | * task to be performed while a previous run is still in progress, the task will only be executed once more (hence 11 | * "debounced"). 12 | * 13 | * @author yawkat 14 | */ 15 | class Debouncer { 16 | val runLock = ReentrantLock() 17 | @Volatile 18 | var requested = false 19 | 20 | inline fun requestRun(task: () -> Unit) { 21 | requested = true 22 | while (requested && runLock.tryLock()) { 23 | try { 24 | if (requested) { 25 | requested = false 26 | task() 27 | } 28 | } finally { 29 | runLock.unlock() 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/crawlers.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import io.undertow.server.HttpServerExchange 4 | import io.undertow.util.Headers 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | internal fun HttpServerExchange.isCrawler(): Boolean { 10 | val agents = requestHeaders[Headers.USER_AGENT] 11 | if (agents.isNullOrEmpty()) { 12 | return false 13 | } 14 | val agent = agents.first 15 | // from https://www.keycdn.com/blog/web-crawlers 16 | return agent.contains("Googlebot") || 17 | agent.contains("Bingbot") || 18 | agent.contains("Slurp") || 19 | agent.contains("DuckDuckBot") || 20 | agent.contains("Baiduspider") || 21 | agent.contains("YandexBot") || 22 | agent.contains("Sogou") || 23 | agent.contains("Exabot") || 24 | agent.contains("facebot") || 25 | agent.contains("facebookexternalhit") || 26 | agent.contains("ia_archiver") || 27 | agent.contains("DotBot") || 28 | agent.contains("SemrushBot") || 29 | agent.contains("AhrefsBot") 30 | } -------------------------------------------------------------------------------- /benchmark-index-data-memory.tsv: -------------------------------------------------------------------------------- 1 | 1 0 381856032 2 | 1 1 580707440 3 | 1 2 800063136 4 | 1 3 882883664 5 | 1 4 869051384 6 | 1 5 783955960 7 | 2 0 244635552 8 | 2 1 390800312 9 | 2 2 557831960 10 | 2 3 629337880 11 | 2 4 630749136 12 | 2 5 575582904 13 | 4 0 173852296 14 | 4 1 295495600 15 | 4 2 443454144 16 | 4 3 521359920 17 | 4 4 539165232 18 | 4 5 500544144 19 | 8 0 138520432 20 | 8 1 252452880 21 | 8 2 414383024 22 | 8 3 524334368 23 | 8 4 557817392 24 | 8 5 518472424 25 | 16 0 136673480 26 | 16 1 269437832 27 | 16 2 495348576 28 | 16 3 630149120 29 | 16 4 652953400 30 | 16 5 593358936 31 | 32 0 158440216 32 | 32 1 344950560 33 | 32 2 637225384 34 | 32 3 790049048 35 | 32 4 800131528 36 | 32 5 709048912 37 | 64 0 217413280 38 | 64 1 479349984 39 | 64 2 870892024 40 | 64 3 1063543848 41 | 64 4 1052605960 42 | 64 5 898394816 43 | 128 0 172771752 44 | 128 1 434967264 45 | 128 2 861924592 46 | 128 3 1086596480 47 | 128 4 1064480688 48 | 128 5 892518480 49 | 256 0 180217264 50 | 256 1 511352920 51 | 256 2 1057230576 52 | 256 3 1324008360 53 | 256 4 1255692560 54 | 256 5 1006982096 55 | 512 0 191940856 56 | 512 1 631440992 57 | 512 2 1397364072 58 | 512 3 1724166616 59 | 512 4 1562997248 60 | 512 5 1172874760 -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/GeneratorSchema.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import at.yawk.javabrowser.loadScript 4 | import org.jdbi.v3.core.Handle 5 | import org.slf4j.LoggerFactory 6 | 7 | private val log = LoggerFactory.getLogger(GeneratorSchema::class.java) 8 | 9 | class GeneratorSchema(private val conn: Handle) { 10 | private fun executeAndLogStatement(stmt: String) { 11 | log.info("{}", stmt) 12 | conn.createUpdate(stmt).execute() 13 | } 14 | 15 | fun createSchema() { 16 | // this is fast, don't log 17 | conn.loadScript("/at/yawk/javabrowser/generator/DataSchema.sql").execute() 18 | } 19 | 20 | fun createIndices() { 21 | val script = conn.loadScript("/at/yawk/javabrowser/generator/DataIndex.sql") 22 | for (statement in script.statements) { 23 | executeAndLogStatement(statement) 24 | } 25 | } 26 | 27 | fun updateViews(concurrent: Boolean) { 28 | for (view in listOf("binding_reference_count_view")) { 29 | executeAndLogStatement("refresh materialized view " + (if (concurrent) "concurrently " else "") + view) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/typesearch/IndexChunkSizeMemoryBenchmark.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.typesearch 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | private fun gc() { 6 | println("Running gc...") 7 | System.runFinalization() 8 | System.gc() 9 | System.runFinalization() 10 | System.gc() 11 | TimeUnit.SECONDS.sleep(2) 12 | } 13 | 14 | fun main(args: Array) { 15 | val entries = benchmarkGetEntries(args) 16 | 17 | for (chunkSize in listOf(1, 2, 4, 8, 16, 32, 64, 128, 256, 512)) { 18 | for (jumps in 0..IndexChunkSizeRuntimeBenchmark.JUMP_MAX) { 19 | gc() 20 | val before = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() 21 | val automaton = IndexAutomaton( 22 | entries, 23 | { it.componentsLower.asList() }, 24 | jumps, chunkSize 25 | ) 26 | gc() 27 | val after = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() 28 | 29 | println("$chunkSize\t$jumps\t${after - before}") 30 | automaton.run("foo") 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/DiffTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.view.DeclarationNode 4 | import com.google.common.base.Equivalence 5 | import org.testng.Assert 6 | import org.testng.annotations.Test 7 | 8 | class DiffTest { 9 | @Test 10 | fun `test buildDiffEdits + mapEdits`() { 11 | val newItems = listOf("a", "a", "b", "d", "e") 12 | val oldItems = listOf("a", "b", "c", "e") 13 | val edits = buildDiffEdits( 14 | newItems, 15 | oldItems, 16 | Equivalence.equals() 17 | ) 18 | val mapped = mapEdits(newItems, oldItems, edits) { a, b -> a to b } 19 | Assert.assertEquals( 20 | mapped, 21 | listOf( 22 | "a" to DeclarationNode.DiffResult.UNCHANGED, 23 | "a" to DeclarationNode.DiffResult.INSERTION, 24 | "b" to DeclarationNode.DiffResult.UNCHANGED, 25 | "c" to DeclarationNode.DiffResult.DELETION, 26 | "d" to DeclarationNode.DiffResult.INSERTION, 27 | "e" to DeclarationNode.DiffResult.UNCHANGED 28 | ) 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/org/eclipse/jdt/internal/compiler/parser/PrepareMonkeyPatch.kt: -------------------------------------------------------------------------------- 1 | package org.eclipse.jdt.internal.compiler.parser 2 | 3 | import java.lang.reflect.Field 4 | import java.lang.reflect.Modifier 5 | import java.util.concurrent.ConcurrentHashMap 6 | 7 | /** 8 | * This object disables class file signature verification so that the monkey patch in AbstractCommentParser works. This 9 | * class must be loaded before AbstractCommentParser. 10 | */ 11 | object PrepareMonkeyPatch { 12 | init { 13 | // run once 14 | 15 | val field = ClassLoader::class.java.getDeclaredField("package2certs") 16 | val modifiersField = Field::class.java.getDeclaredField("modifiers") 17 | modifiersField.isAccessible = true 18 | modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) 19 | field.isAccessible = true 20 | field.set(javaClass.classLoader, object : ConcurrentHashMap() { 21 | override fun putIfAbsent(key: Any, value: Any): Any? { 22 | return null 23 | } 24 | 25 | override fun put(key: Any, value: Any): Any? { 26 | return null 27 | } 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/TreeIterator.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import com.google.common.collect.PeekingIterator 4 | 5 | /** 6 | * @author yawkat 7 | */ 8 | abstract class TreeIterator(protected val flatDelegate: PeekingIterator) : Iterator { 9 | private var eaten = false 10 | private var toEat: TreeIterator<*, *>? = null 11 | 12 | override fun hasNext(): Boolean { 13 | if (eaten) throw IllegalStateException("Iterator already eaten") 14 | 15 | // consume the toEat iterator so we're sure we reached the end of the previous subtree 16 | toEat?.forEach { _ -> } 17 | toEat?.eaten = true 18 | toEat = null 19 | 20 | if (!flatDelegate.hasNext()) return false 21 | return !returnToParent(flatDelegate.peek()) 22 | } 23 | 24 | override fun next(): E { 25 | return mapOneItem() 26 | } 27 | 28 | protected fun registerSubIterator(subIterator: TreeIterator<*, *>) { 29 | if (toEat != null) throw IllegalStateException() 30 | toEat = subIterator 31 | } 32 | 33 | protected abstract fun mapOneItem(): E 34 | 35 | protected abstract fun returnToParent(item: O): Boolean 36 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/ListUpdates.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import org.jboss.shrinkwrap.resolver.api.NoResolvedResultException 4 | import org.jboss.shrinkwrap.resolver.api.maven.Maven 5 | import java.nio.file.Paths 6 | 7 | fun main(args: Array) { 8 | val cfg = Config.fromFile(Paths.get(args[0])) 9 | val mavenArtifacts = cfg.artifacts 10 | .filterIsInstance() 11 | .groupBy { "${it.groupId}:${it.artifactId}" } 12 | .mapValues { (_, v) -> v.mapTo(HashSet()) { it.version } } 13 | for ((qualifier, versions) in mavenArtifacts) { 14 | if (qualifier.startsWith("commons-")) continue 15 | val resolved = try { 16 | Maven.configureResolver() 17 | .withMavenCentralRepo(true) 18 | .resolve("$qualifier:LATEST").withoutTransitivity().asSingleResolvedArtifact() 19 | } catch (e: NoResolvedResultException) { 20 | println(e) 21 | continue 22 | } 23 | val latestVersion = resolved.resolvedVersion 24 | if (latestVersion !in versions) { 25 | println("$qualifier $latestVersion <- $versions") 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/EscaperTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.DataProvider 5 | import org.testng.annotations.Test 6 | import java.io.StringWriter 7 | 8 | /** 9 | * @author yawkat 10 | */ 11 | class EscaperTest { 12 | @DataProvider 13 | fun testStrings(): Array> = arrayOf( 14 | arrayOf("abcghi&jkl'mno\"pqr", "abc<def>ghi&jkl'mno"pqr") 15 | ) 16 | 17 | @Test(dataProvider = "testStrings") 18 | fun test(unescaped: String, escaped: String) { 19 | Assert.assertEquals(Escaper.HTML.escape(unescaped), escaped) 20 | } 21 | 22 | @Test(dataProvider = "testStrings") 23 | fun testWriter(unescaped: String, escaped: String) { 24 | val writer = StringWriter() 25 | Escaper.HTML.escape(writer, unescaped) 26 | Assert.assertEquals(writer.toString(), escaped) 27 | } 28 | 29 | @Test(dataProvider = "testStrings") 30 | fun testRange(unescaped: String, escaped: String) { 31 | Assert.assertEquals(Escaper.HTML.escape("fo'<'o" + unescaped + "bar", start = 6, end = unescaped.length + 6), 32 | escaped) 33 | } 34 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/VersionComparator.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | /** 4 | * Compares dot/slash-separated strings numerically and alphabetically, placing "later" numerical values first. 5 | * 6 | * 'java/8' > 'java/11' 7 | */ 8 | object VersionComparator : Comparator { 9 | override fun compare(o1: String, o2: String): Int { 10 | val p1 = o1.split('.', '/') 11 | val p2 = o2.split('.', '/') 12 | var i = 0 13 | while (true) { 14 | if (i >= p1.size) { 15 | if (i >= p2.size) return 0 16 | return -1 17 | } 18 | if (i >= p2.size) { 19 | return 1 20 | } 21 | 22 | val e1 = p1[i] 23 | val e2 = p2[i] 24 | if (e1 != e2) { 25 | try { 26 | val k1 = e1.toLong() 27 | val k2 = e2.toLong() 28 | // newer first 29 | return -java.lang.Long.compare(k1, k2) 30 | } catch (e: NumberFormatException) { 31 | return e1.compareTo(e2) 32 | } 33 | } 34 | 35 | i++ 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/typesearch/BindingTokenizer.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.typesearch 2 | 3 | import java.util.Locale 4 | import java.util.regex.Pattern 5 | 6 | private fun splitByPattern(s: String, splitPattern: Pattern): List { 7 | val out = ArrayList() 8 | val matcher = splitPattern.matcher(s) 9 | var last = 0 10 | while (matcher.find()) { 11 | val end = matcher.start() + 1 12 | out.add(s.substring(last, end).toLowerCase(Locale.US)) 13 | last = end 14 | } 15 | if (last != s.length) { 16 | out.add(s.substring(last).toLowerCase(Locale.US)) 17 | } 18 | return out 19 | } 20 | 21 | interface BindingTokenizer { 22 | fun tokenize(s: String): List 23 | 24 | object Java : BindingTokenizer { 25 | private val JAVA_SPLIT_PATTERN = Pattern.compile("(?:\\.|[a-z0-9][A-Z]|[a-zA-Z][0-9])") 26 | 27 | override fun tokenize(s: String) = splitByPattern(s, JAVA_SPLIT_PATTERN) 28 | } 29 | 30 | object Bytecode : BindingTokenizer { 31 | private val BYTECODE_SPLIT_PATTERN = Pattern.compile("(?:/|^L|[a-z0-9][A-Z]|[a-zA-Z][0-9])") 32 | 33 | override fun tokenize(s: String) = splitByPattern(s, BYTECODE_SPLIT_PATTERN) 34 | } 35 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/work/PrepareArtifactWorker.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.generator.ArtifactConfig 5 | import at.yawk.javabrowser.generator.SourceSetConfig 6 | 7 | interface PrepareArtifactWorker { 8 | fun getArtifactId(config: A): String 9 | 10 | /** 11 | * Prepare an artifact for compilation, and run [PrepareListener.compileSourceSet] for all of its source sets. [PrepareListener.compileSourceSet] must only return once compilation of that source set is complete – the source set becomes invalid once the [PrepareListener.compileSourceSet] call terminates. 12 | * 13 | * [PrepareListener.acceptMetadata] must be called exactly once, before any [PrepareListener.compileSourceSet] calls. 14 | */ 15 | suspend fun prepareArtifact( 16 | artifactId: String, 17 | config: A, 18 | listener: PrepareListener 19 | ) 20 | 21 | interface PrepareListener { 22 | fun acceptMetadata(metadata: Metadata) 23 | 24 | suspend fun compileSourceSet(config: SourceSetConfig) 25 | } 26 | 27 | data class Metadata( 28 | val dependencyArtifactIds: Collection, 29 | val aliases: Collection = emptyList(), 30 | val artifactMetadata: ArtifactMetadata 31 | ) 32 | } -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/DbConfig.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.zaxxer.hikari.HikariConfig 4 | import com.zaxxer.hikari.HikariDataSource 5 | import org.jdbi.v3.core.Handle 6 | import org.jdbi.v3.core.Jdbi 7 | import org.jdbi.v3.sqlobject.SqlObjectPlugin 8 | 9 | /** 10 | * @author yawkat 11 | */ 12 | data class DbConfig( 13 | val url: String, 14 | val user: String, 15 | val password: String 16 | ) { 17 | enum class Mode { 18 | FRONTEND, 19 | GENERATOR, 20 | } 21 | 22 | fun start(mode: Mode, closure: (HikariConfig) -> Unit = {}): Jdbi { 23 | val hikariConfig = HikariConfig() 24 | hikariConfig.jdbcUrl = url 25 | hikariConfig.username = user 26 | hikariConfig.password = System.getProperty("at.yawk.javabrowser.db-password", password) 27 | if (mode == Mode.FRONTEND) { 28 | hikariConfig.connectionInitSql = "set search_path to interactive, data" 29 | } else { 30 | hikariConfig.connectionInitSql = "set search_path to data" 31 | } 32 | closure(hikariConfig) 33 | val dataSource = HikariDataSource(hikariConfig) 34 | 35 | return Jdbi.create(dataSource).installPlugin(SqlObjectPlugin()) 36 | } 37 | } 38 | 39 | fun Handle.loadScript(path: String) = createScript(DbConfig::class.java.getResourceAsStream(path).use { it!!.bufferedReader().readText() }) -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/bytecode/SignatureTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | class SignatureTest { 7 | @Test 8 | fun `method signature 1`() { 9 | val printer = BytecodePrinter(::testHashBinding) 10 | printer.printMethodSignature("(Ljava/util/List;I)Ljava/util/List;^TE;^Ljava/lang/Throwable;", 11 | access = 0, 12 | name = "test") 13 | 14 | Assert.assertEquals(printer.finishString(), 15 | " java.util.List test(java.util.List, int) throws E, java.lang.Throwable") 16 | } 17 | 18 | @Test 19 | fun `method signature 2`() { 20 | val printer = BytecodePrinter(::testHashBinding) 21 | printer.printMethodSignature("()V^TE;", access = 0, name = "test") 22 | 23 | Assert.assertEquals(printer.finishString(), " void test() throws E") 24 | } 25 | 26 | @Test 27 | fun `method signature 3`() { 28 | val printer = BytecodePrinter(::testHashBinding) 29 | printer.printMethodSignature("()V", access = 0, name = "test") 30 | 31 | Assert.assertEquals(printer.finishString(), " void test()") 32 | } 33 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ConservativeLoopBlock.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import freemarker.core.Environment 4 | import freemarker.ext.util.WrapperTemplateModel 5 | import freemarker.template.TemplateBooleanModel 6 | import freemarker.template.TemplateDirectiveBody 7 | import freemarker.template.TemplateDirectiveModel 8 | import freemarker.template.TemplateModel 9 | import freemarker.template.TemplateModelException 10 | 11 | /** 12 | * Freemarker loop block for Iterators that does not use previous elements, hasNext etc. 13 | * 14 | * @author yawkat 15 | */ 16 | class ConservativeLoopBlock : TemplateDirectiveModel { 17 | override fun execute( 18 | env: Environment, 19 | params: MutableMap, 20 | loopVars: Array, 21 | body: TemplateDirectiveBody?) { 22 | val iterator = (params["iterator"] as? WrapperTemplateModel)?.wrappedObject as? Iterator 23 | ?: throw TemplateModelException("`iterator` must be an Iterator") 24 | val skipNull = params["skipNull"] != null && (params["skipNull"] as TemplateBooleanModel).asBoolean 25 | if (body != null) { 26 | for (item in iterator) { 27 | if (!skipNull || item != null) { 28 | loopVars[0] = env.objectWrapper.wrap(item) 29 | body.render(env.out) 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/ArtifactMetadata.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | 5 | /** 6 | * @author yawkat 7 | */ 8 | @JsonIgnoreProperties(ignoreUnknown = true) 9 | data class ArtifactMetadata( 10 | val logoUrl: String? = null, 11 | val licenses: List? = null, 12 | val url: String? = null, 13 | val description: String? = null, 14 | val issueTracker: IssueTracker? = null, 15 | val organization: Organization? = null, 16 | val developers: List? = null, 17 | val contributors: List? = null 18 | ) { 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | data class License( 21 | val name: String, 22 | val url: String? 23 | ) 24 | 25 | @JsonIgnoreProperties(ignoreUnknown = true) 26 | data class Organization( 27 | val name: String? = null, 28 | val url: String? = null 29 | ) 30 | 31 | @JsonIgnoreProperties(ignoreUnknown = true) 32 | data class Developer( 33 | val name: String? = null, 34 | val email: String? = null, 35 | val url: String? = null, 36 | val organization: Organization? = null 37 | ) 38 | 39 | @JsonIgnoreProperties(ignoreUnknown = true) 40 | data class IssueTracker( 41 | val type: String? = null, 42 | val url: String? = null 43 | ) 44 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/ListMissingDependencies.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenCoordinate 4 | import java.nio.file.Paths 5 | import java.util.TreeSet 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | private fun matches(coordinate: MavenCoordinate, it: ArtifactConfig.Maven): Boolean = 11 | (it.groupId == coordinate.groupId && it.artifactId == coordinate.artifactId) || 12 | it.aliases.any { alias -> matches(coordinate, alias) } 13 | 14 | fun main(args: Array) { 15 | val cfg = Config.fromFile(Paths.get(args[0])) 16 | val dependencyResolver = MavenDependencyResolver(cfg.mavenResolver) 17 | val mavenArtifacts = cfg.artifacts.filterIsInstance() 18 | val missing = TreeSet() 19 | for (artifact in mavenArtifacts) { 20 | val dependencies = dependencyResolver.getMavenDependencies( 21 | artifact.groupId, 22 | artifact.artifactId, 23 | artifact.version) 24 | 25 | for (dependency in dependencies) { 26 | if (mavenArtifacts.none { matches(dependency.coordinate, it) }) { 27 | val s = """maven("${dependency.coordinate.groupId}", "${dependency.coordinate.artifactId}", "${dependency.coordinate.version}")""" 28 | missing.add(s) 29 | } 30 | } 31 | } 32 | for (s in missing) { 33 | println(s) 34 | } 35 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/fullTextSearchResultView.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.FullTextSearchResultView" --> 3 | <#import "page.ftl" as page> 4 | <#import "fullTextSearchForm.ftl" as ftsf> 5 | <#macro resultPart result> 6 |
<@result.renderNextRegionDirective/>
7 | <#if result.hasMore> 8 | <@resultPart result/> 9 | 10 | 11 | <@page.page title="Full Text Search" realm=searchRealm!'source' newPath=path! additionalTitle="Full Text Search" hasSearch=false narrow=false tooltip=false> 12 |
13 | <@ftsf.fullTextSearchForm query=query searchArtifact=searchArtifact!'' /> 14 | 15 | <#if searchArtifact?has_content> 16 | Search everywhere 17 | 18 | 19 | <#if results?has_content> 20 |
    21 | <@ConservativeLoopBlock iterator=results; result> 22 | <#-- @ftlvariable name="result" type="at.yawk.javabrowser.server.view.FullTextSearchResultView.SourceFileResult" --> 23 |
  • 24 |

    <#if !searchArtifact??>${result.artifactId} / ${result.path}

    25 | <@resultPart result/> 26 |
  • 27 | 28 |
29 | <#else> 30 |

No results

31 | 32 |
33 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ArtifactMetadataCache.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.server.artifact.ArtifactNode 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.google.common.cache.CacheBuilder 7 | import com.google.common.cache.CacheLoader 8 | import org.jdbi.v3.core.Handle 9 | import org.jdbi.v3.core.Jdbi 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class ArtifactMetadataCache @Inject constructor( 15 | private val objectMapper: ObjectMapper, 16 | private val dbi: Jdbi, 17 | artifactUpdater: ArtifactUpdater 18 | ) { 19 | private val cache = CacheBuilder.newBuilder() 20 | .build(object : CacheLoader() { 21 | override fun load(key: Long): ArtifactMetadata { 22 | return dbi.inTransaction { conn: Handle -> 23 | val bytes = conn.select("select metadata from artifact where artifact_id = ?", key).mapToMap() 24 | .single()["metadata"] as ByteArray 25 | objectMapper.readValue(bytes, ArtifactMetadata::class.java) 26 | } 27 | } 28 | }) 29 | 30 | init { 31 | artifactUpdater.addArtifactUpdateListener { cache.invalidate(it) } 32 | } 33 | 34 | fun getArtifactMetadata(node: ArtifactNode) = cache.get(node.dbId!!)!! 35 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/source-file.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.SourceFileView" --> 3 | <#import "page.ftl" as page> 4 | <#import "declarationNode.ftl" as declarationNode> 5 | <#import "alternatives.ftl" as at> 6 | <#import "diffStats.ftl" as diffStats> 7 | 8 | <#assign additionalMenu> 9 | <@at.alternatives alternatives/> 10 | 11 | <@page.page title="${newInfo.sourceFilePath.artifact.stringId} : ${newInfo.sourceFilePath.sourceFilePath}" realm=newInfo.realm newPath=newInfo.sourceFilePath oldPath=(oldInfo.sourceFilePath)! hasSearch=true additionalMenu=additionalMenu tooltip=true narrow=false> 12 | 13 |
14 |
15 |
    16 | <@ConservativeLoopBlock iterator=declarations; declaration> 17 |
  • 18 | <@declarationNode.declarationNode declaration/> 19 |
  • 20 | 21 |
22 |
23 | 24 |
25 | <#include "metadata.ftl"> 26 | 27 | <#if oldInfo??> 28 | <@diffStats.diffStats newInfo.sourceFilePath oldInfo.sourceFilePath/> 29 | 30 | +${diff.insertions} 31 | -${diff.deletions} 32 | 33 | 34 | 35 |
<@printerDirective/>
36 |
37 |
38 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/work/TempDirProvider.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import com.google.common.io.MoreFiles 4 | import kotlinx.coroutines.sync.Semaphore 5 | import kotlinx.coroutines.sync.withPermit 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | 9 | internal inline fun tempDir0(base: Path?, name: String, f: (Path) -> R): R { 10 | val sanitizedName = name.replace("[^a-zA-Z0-9._]".toRegex(), "_") 11 | val tmp = 12 | if (base == null) Files.createTempDirectory(sanitizedName) 13 | else Files.createTempDirectory(base, sanitizedName) 14 | try { 15 | return f(tmp) 16 | } finally { 17 | @Suppress("UnstableApiUsage") 18 | MoreFiles.deleteRecursively(tmp) 19 | } 20 | } 21 | 22 | interface TempDirProvider { 23 | suspend fun withTempDir(debugTag: String, f: suspend (Path) -> R): R 24 | 25 | class FromDirectory(private val base: Path) : TempDirProvider { 26 | override suspend fun withTempDir(debugTag: String, f: suspend (Path) -> R) = 27 | tempDir0(base, debugTag) { f(it) } 28 | } 29 | 30 | class Limited( 31 | private val delegate: TempDirProvider, 32 | maxOpen: Int = 16 33 | ) : TempDirProvider { 34 | private val semaphore = Semaphore(maxOpen) 35 | 36 | override suspend fun withTempDir(debugTag: String, f: suspend (Path) -> R): R { 37 | semaphore.withPermit { 38 | return delegate.withTempDir(debugTag, f) 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/InitInteractiveSchema.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists interactive; 2 | alter table if exists public.hits set schema interactive; 3 | create table if not exists interactive.hits 4 | ( 5 | -- UTC timestamp of this hit counter. Resolution is application-defined, and may be multiple hours. 6 | timestamp timestamp not null, 7 | sourceFile varchar not null, 8 | artifactId varchar not null, 9 | hits int8 not null, 10 | 11 | primary key (timestamp, sourceFile, artifactId) 12 | ); 13 | 14 | -- the common_prefix is only used on items from the data schema, but because we use it in the server, we put it in the 15 | -- interactive schema so we can update it easily 16 | create or replace function interactive.common_prefix_iterate(state text, value text) 17 | returns text 18 | as 19 | $$ 20 | select substr(value, 0, len) as new_prefix 21 | from generate_series(0, least(length(state), length(value)) + 1) len 22 | where substr(value, 0, len) = substr(state, 0, len) 23 | order by len desc 24 | limit 1 25 | $$ language 'sql' immutable strict; 26 | 27 | create or replace aggregate interactive.common_prefix(text) ( 28 | sfunc = common_prefix_iterate, 29 | stype = text, 30 | combinefunc = common_prefix_iterate, 31 | parallel = safe 32 | ); 33 | 34 | create or replace aggregate xor(int8) ( 35 | stype = int8, 36 | sfunc = int8xor, 37 | mstype = int8, 38 | msfunc = int8xor, 39 | minvfunc = int8xor, 40 | combinefunc = int8xor, 41 | initcond = '0', 42 | minitcond = '0', 43 | parallel = safe 44 | ); 45 | -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/typesearch/IndexAutomatonTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.typesearch 2 | 3 | import org.testng.Assert 4 | import org.testng.annotations.Test 5 | 6 | /** 7 | * @author yawkat 8 | */ 9 | class IndexAutomatonTest { 10 | @Test 11 | fun partial() { 12 | val automaton = IndexAutomaton( 13 | listOf("java.util.concurrent.ConcurrentHashMap", 14 | "java.util.HashMap"), 15 | { BindingTokenizer.Java.tokenize(it) }, 16 | 4 17 | ) 18 | val result = automaton.run("javuticohamap") 19 | Assert.assertEquals( 20 | result.asSequence().toList(), 21 | listOf("java.util.concurrent.ConcurrentHashMap") 22 | ) 23 | } 24 | 25 | @Test 26 | fun `jump direct`() { 27 | val automaton = IndexAutomaton( 28 | listOf("io.netty.buffer.ByteBuf"), 29 | { BindingTokenizer.Java.tokenize(it) }, 30 | 1 31 | ) 32 | val result = automaton.run("nettybytebuf") 33 | Assert.assertEquals( 34 | result.asSequence().toList(), 35 | listOf("io.netty.buffer.ByteBuf") 36 | ) 37 | } 38 | 39 | @Test 40 | fun `no jump`() { 41 | val automaton = IndexAutomaton( 42 | listOf("java.lang.String"), 43 | { BindingTokenizer.Java.tokenize(it) }, 44 | 0 45 | ) 46 | val result = automaton.run("string") 47 | Assert.assertEquals( 48 | result.asSequence().toList(), 49 | listOf("java.lang.String") 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /shared/src/test/kotlin/at/yawk/javabrowser/SourceAnnotationTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.KotlinModule 5 | import com.fasterxml.jackson.module.kotlin.SingletonSupport 6 | import org.testng.Assert 7 | import org.testng.annotations.Test 8 | 9 | class SourceAnnotationTest { 10 | private val objectMapper = ObjectMapper().registerModule(KotlinModule(singletonSupport = SingletonSupport.CANONICALIZE)) 11 | 12 | private inline fun testDeser(o: T) { 13 | val s = objectMapper.writerFor(T::class.java).writeValueAsString(o) 14 | val d = objectMapper.readerFor(T::class.java).readValue(s) 15 | Assert.assertEquals(d, o) 16 | } 17 | 18 | @Test 19 | fun testDeser() { 20 | testDeser(BindingRef( 21 | BindingRefType.SUPER_TYPE, 22 | BindingId(123), 23 | 234, 24 | false 25 | )) 26 | testDeser(BindingDecl( 27 | id = BindingId(123), 28 | binding = "abc", 29 | description = BindingDecl.Description.Package, 30 | modifiers = 456, 31 | superBindings = emptyList(), 32 | parent = BindingId(789), 33 | corresponding = mapOf(Realm.BYTECODE to BindingId(456)) 34 | )) 35 | testDeser(BindingDecl.Description.Type( 36 | BindingDecl.Description.Type.Kind.CLASS, 37 | BindingId(123), 38 | "abc", 39 | emptyList() 40 | )) 41 | testDeser(BindingDecl.Super( 42 | "abc", 43 | BindingId(123) 44 | )) 45 | } 46 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ServerSourceFile.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.BindingDecl 4 | import at.yawk.javabrowser.PositionedAnnotation 5 | import com.fasterxml.jackson.core.JsonToken 6 | import java.io.IOException 7 | import java.nio.charset.StandardCharsets 8 | 9 | /** 10 | * @author yawkat 11 | */ 12 | class ServerSourceFile(val text: String, annotations: Sequence) { 13 | var annotations: Sequence = annotations 14 | private set 15 | val declarations: Iterator 16 | get() = annotations.mapNotNull { it.annotation as? BindingDecl }.iterator() 17 | 18 | constructor( 19 | textBytes: ByteArray, 20 | annotationBytes: ByteArray 21 | ) : this(textBytes.toString(StandardCharsets.UTF_8), lazyParseAnnotations(annotationBytes)) 22 | 23 | @Deprecated("try to use #entries instead") 24 | val annotationList by lazy(LazyThreadSafetyMode.NONE) { 25 | val l = annotations.toList() 26 | this.annotations = l.asSequence() 27 | l 28 | } 29 | 30 | fun bakeAnnotations() { 31 | annotationList 32 | } 33 | 34 | companion object { 35 | fun lazyParseAnnotations(annotationBytes: ByteArray) = sequence { 36 | val parser = cborMapper.factory.createParser(annotationBytes) 37 | if (parser.nextToken() != JsonToken.START_ARRAY) throw IOException() 38 | while (parser.nextToken() != JsonToken.END_ARRAY) { 39 | yield(cborMapper.readValue(parser, PositionedAnnotation::class.java)!!) 40 | } 41 | if (parser.nextToken() != null) throw IOException("trailing tokens") 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/source/ModuleFileParser.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.source 2 | 3 | import org.eclipse.jdt.core.JavaCore 4 | import org.eclipse.jdt.core.dom.AST 5 | import org.eclipse.jdt.core.dom.ASTParser 6 | import org.eclipse.jdt.core.dom.CompilationUnit 7 | import org.eclipse.jdt.core.dom.FileASTRequestor 8 | import org.eclipse.jdt.core.dom.ModuleDeclaration 9 | import org.eclipse.jdt.core.dom.RequiresDirective 10 | import org.eclipse.jdt.internal.compiler.parser.PrepareMonkeyPatch 11 | import java.nio.file.Path 12 | 13 | fun parseModuleFile(path: Path): ModuleDeclaration { 14 | PrepareMonkeyPatch 15 | 16 | var moduleDeclaration: ModuleDeclaration? = null 17 | val parser = ASTParser.newParser(AST.JLS_Latest) 18 | parser.setCompilerOptions(mapOf( 19 | JavaCore.COMPILER_SOURCE to JavaCore.latestSupportedJavaVersion(), 20 | JavaCore.CORE_ENCODING to "UTF-8" 21 | )) 22 | parser.setKind(ASTParser.K_COMPILATION_UNIT) 23 | parser.createASTs( 24 | arrayOf(path.toAbsolutePath().toString()), 25 | arrayOf("UTF-8"), 26 | emptyArray(), 27 | object : FileASTRequestor() { 28 | override fun acceptAST(sourceFilePath: String, ast: CompilationUnit) { 29 | moduleDeclaration = ast.module 30 | } 31 | }, 32 | null 33 | ) 34 | return moduleDeclaration ?: throw IllegalArgumentException("Not a module file: $path") 35 | } 36 | 37 | fun getRequiredModules(declaration: ModuleDeclaration): Set { 38 | return declaration.moduleStatements() 39 | .filterIsInstance() 40 | .map { it.name.fullyQualifiedName } 41 | .toSet() 42 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/AliasIndex.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import com.google.common.collect.ArrayListMultimap 4 | import com.google.common.collect.Multimap 5 | import org.jdbi.v3.core.Handle 6 | import org.jdbi.v3.core.Jdbi 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class AliasIndex @Inject constructor( 12 | artifactUpdater: ArtifactUpdater, 13 | dbi: Jdbi 14 | ) { 15 | @Volatile 16 | private var aliases: Multimap = ArrayListMultimap.create() 17 | 18 | init { 19 | artifactUpdater.addInvalidationListener(true) { 20 | dbi.inTransaction { conn: Handle -> 21 | val newAliases = ArrayListMultimap.create() 22 | for (row in conn.select("select artifact_id, alias from artifact_alias").mapToMap()) { 23 | newAliases.put((row["artifact_id"] as Number).toLong(), row["alias"] as String) 24 | } 25 | this.aliases = newAliases 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Find an artifact that is aliased to the requested ID or an ID where only the version differs. 32 | */ 33 | fun findAliasedTo(requestedId: String): Long? { 34 | for ((actual, alias) in aliases.entries()) { 35 | if (alias == requestedId) { 36 | return actual 37 | } 38 | } 39 | 40 | val prefixWithoutId = requestedId.substring(0, requestedId.lastIndexOf('/') + 1) 41 | for ((actual, alias) in aliases.entries()) { 42 | if (alias.startsWith(prefixWithoutId)) { 43 | return actual 44 | } 45 | } 46 | 47 | return null 48 | } 49 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/typesearch/IndexChunkSizeRuntimeBenchmark.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.typesearch 2 | 3 | import com.google.common.io.MoreFiles 4 | import org.jdbi.v3.core.Jdbi 5 | import org.openjdk.jmh.runner.Runner 6 | import org.openjdk.jmh.runner.options.OptionsBuilder 7 | import org.openjdk.jmh.runner.options.TimeValue 8 | import java.nio.file.Files 9 | import java.nio.file.Paths 10 | 11 | fun benchmarkGetEntries(args: Array): List { 12 | val dbi = Jdbi.open(args[0]) 13 | return dbi.createQuery("select binding from data.bindings where realm = 0 and isType") 14 | .map { rs, _, _ -> 15 | SearchIndex.SplitEntry(rs.getString(1), BindingTokenizer.Java) 16 | } 17 | .toList() 18 | .sorted() 19 | } 20 | 21 | fun main(args: Array) { 22 | val entries = benchmarkGetEntries(args) 23 | 24 | val automataDir = Files.createTempDirectory(Paths.get("/var/tmp"), "automata") 25 | try { 26 | val bindingsFile = automataDir.resolve("bindings.txt") 27 | Files.write(bindingsFile, entries.map { it.string }) 28 | 29 | println("Running benchmark...") 30 | val options = OptionsBuilder() 31 | .forks(1) 32 | .warmupIterations(5) 33 | .warmupTime(TimeValue.seconds(10)) 34 | .measurementIterations(10) 35 | .measurementTime(TimeValue.seconds(10)) 36 | .include(IndexChunkSizeRuntimeBenchmark::class.java.name) 37 | .param("automataDir", automataDir.toString()) 38 | .jvmArgsPrepend("-Xmx10G", "-Xms10G") 39 | .build() 40 | 41 | Runner(options).run() 42 | } finally { 43 | MoreFiles.deleteRecursively(automataDir) 44 | } 45 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/MultiSemaphoreTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.asCoroutineDispatcher 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.runBlocking 7 | import org.testng.Assert 8 | import org.testng.annotations.Test 9 | import java.util.concurrent.Executors 10 | 11 | class MultiSemaphoreTest { 12 | @Test 13 | fun test() { 14 | val sem = MultiSemaphore(4) 15 | 16 | var stage = 0 17 | fun expectStage(s: Int) { 18 | println(s) 19 | Assert.assertEquals(stage, s) 20 | stage = s + 1 21 | } 22 | 23 | suspend fun cr1() { 24 | expectStage(0) 25 | 26 | sem.acquire(1) 27 | expectStage(1) 28 | 29 | sem.acquire(2) 30 | expectStage(2) 31 | 32 | sem.acquire(2) 33 | expectStage(4) 34 | sem.release(1) 35 | 36 | sem.acquire(2) 37 | expectStage(7) 38 | sem.release(1) 39 | } 40 | 41 | suspend fun cr2() { 42 | expectStage(3) 43 | sem.release(1) 44 | 45 | sem.acquire(1) 46 | expectStage(5) 47 | sem.release(1) 48 | 49 | expectStage(6) 50 | sem.release(1) 51 | 52 | sem.acquire(1) 53 | expectStage(8) 54 | } 55 | 56 | val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() 57 | val scope = CoroutineScope(dispatcher) 58 | 59 | val a1 = scope.async { cr1() } 60 | val a2 = scope.async { cr2() } 61 | 62 | runBlocking { 63 | a2.await() 64 | a1.await() 65 | } 66 | 67 | expectStage(9) 68 | } 69 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/ArtifactConfig.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import com.fasterxml.jackson.annotation.JsonSubTypes 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo 6 | import java.net.URL 7 | 8 | /** 9 | * @author yawkat 10 | */ 11 | @JsonTypeInfo( 12 | use = JsonTypeInfo.Id.NAME, 13 | include = JsonTypeInfo.As.PROPERTY, 14 | property = "type" 15 | ) 16 | @JsonSubTypes(value = [ 17 | JsonSubTypes.Type(value = ArtifactConfig.Java::class, name = "java"), 18 | JsonSubTypes.Type(value = ArtifactConfig.Maven::class, name = "maven"), 19 | JsonSubTypes.Type(value = ArtifactConfig.Android::class, name = "android") 20 | ]) 21 | sealed class ArtifactConfig { 22 | abstract val metadata: ArtifactMetadata? 23 | 24 | data class Java( 25 | val version: String, 26 | val archiveUrl: URL, 27 | val jigsaw: Boolean, 28 | override val metadata: ArtifactMetadata 29 | ) : ArtifactConfig() 30 | 31 | data class Maven( 32 | val groupId: String, 33 | val artifactId: String, 34 | val version: String, 35 | override val metadata: ArtifactMetadata? = null, 36 | val aliases: List = emptyList() 37 | ) : ArtifactConfig() 38 | 39 | data class GitRepo( 40 | val url: URL, 41 | val tag: String 42 | ) 43 | 44 | data class Android( 45 | val repos: List, 46 | val version: String, 47 | val buildTools: URL, 48 | override val metadata: ArtifactMetadata 49 | ) : ArtifactConfig() 50 | 51 | data class Graal( 52 | val version: String, 53 | val repos: List, 54 | val jdk: URL, 55 | override val metadata: ArtifactMetadata 56 | ) : ArtifactConfig() 57 | } -------------------------------------------------------------------------------- /server/src/main/resources/assets/references.css: -------------------------------------------------------------------------------- 1 | 2 | .reference-detail-table { 3 | padding: 1em; 4 | margin: 1em; 5 | border: 1px solid; 6 | } 7 | .reference-detail-table table { 8 | text-align: right; 9 | border-collapse: collapse; 10 | } 11 | .reference-detail-table td, .reference-detail-table th { 12 | padding: .2em; 13 | } 14 | 15 | ul.reference-detail-list ul { 16 | margin-left: 1em; 17 | } 18 | 19 | /* Lines */ 20 | ul.reference-detail-list .source-file { 21 | padding-left: 2em; 22 | } 23 | ul.reference-detail-list .source-file h4 { 24 | display: inline-block; 25 | text-indent: -2em; 26 | font-size: 1em; 27 | font-weight: normal; 28 | } 29 | 30 | @media (min-height: 400px) { 31 | ul.reference-detail-list .source-file { 32 | padding-left: 4em; 33 | } 34 | ul.reference-detail-list .source-file h4 { 35 | text-indent: -4em; 36 | } 37 | } 38 | 39 | ul.reference-detail-list .source-file ul { 40 | display: inline-block; 41 | margin: 0; 42 | } 43 | ul.reference-detail-list .source-file li { 44 | display: inline-block; 45 | } 46 | 47 | .reference-detail-list h2, .reference-detail-list h3 { 48 | position: sticky; 49 | } 50 | 51 | ul.reference-detail-list h2 { 52 | top: 0; 53 | z-index: 2; 54 | } 55 | 56 | ul.reference-detail-list h3 { 57 | top: 1.1em; 58 | z-index: 1; 59 | overflow-wrap: break-word; 60 | max-width: 100%; 61 | } 62 | 63 | /* Adjust for top bar on desktop */ 64 | @media (min-height: 400px) { 65 | ul.reference-detail-list h2 { 66 | top: 45px; 67 | } 68 | 69 | ul.reference-detail-list h3 { 70 | top: calc(45px + 1.1em); 71 | } 72 | } 73 | 74 | ul.reference-detail-list h4 { 75 | overflow-wrap: break-word; 76 | max-width: 100%; 77 | } 78 | 79 | .hit-result-limit { 80 | display: block; 81 | font-weight: bold; 82 | padding: 1em; 83 | margin: 1em; 84 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/GeneratorConfigTypeModule.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import com.fasterxml.jackson.core.JsonParser 4 | import com.fasterxml.jackson.core.Version 5 | import com.fasterxml.jackson.databind.BeanDescription 6 | import com.fasterxml.jackson.databind.DeserializationConfig 7 | import com.fasterxml.jackson.databind.DeserializationContext 8 | import com.fasterxml.jackson.databind.JavaType 9 | import com.fasterxml.jackson.databind.JsonDeserializer 10 | import com.fasterxml.jackson.databind.Module 11 | import com.fasterxml.jackson.databind.deser.Deserializers 12 | import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenCoordinate 13 | import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenCoordinates 14 | 15 | /** 16 | * @author yawkat 17 | */ 18 | object GeneratorConfigTypeModule : Module() { 19 | override fun getModuleName() = "GeneratorConfigTypeModule" 20 | 21 | override fun version() = Version.unknownVersion()!! 22 | 23 | override fun setupModule(context: SetupContext) { 24 | context.addDeserializers(object : Deserializers.Base() { 25 | override fun findBeanDeserializer(type: JavaType, 26 | config: DeserializationConfig, 27 | beanDesc: BeanDescription): JsonDeserializer<*>? { 28 | if (type.rawClass == MavenCoordinate::class.java) { 29 | return MavenCoordinateDeserializer 30 | } 31 | return super.findBeanDeserializer(type, config, beanDesc) 32 | } 33 | }) 34 | } 35 | 36 | private object MavenCoordinateDeserializer : JsonDeserializer() { 37 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): MavenCoordinate { 38 | return MavenCoordinates.createCoordinate(p.text) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/Transaction.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.BindingRefType 5 | import at.yawk.javabrowser.Realm 6 | import at.yawk.javabrowser.TsVector 7 | import org.intellij.lang.annotations.Language 8 | 9 | interface Transaction { 10 | suspend fun insertArtifact(id: Long, stringId: String, compilerVersion: Int, metaBytes: ByteArray): Unit 11 | 12 | suspend fun insertDependency(from: Long, to: String) 13 | 14 | suspend fun insertAlias(from: Long, alias: String) 15 | 16 | suspend fun insertSourceFile( 17 | realm: Realm, 18 | artifactId: Long, 19 | sourceFileId: Long, 20 | hash: Long, 21 | path: String, 22 | textBytes: ByteArray, 23 | annotationBytes: ByteArray 24 | ) 25 | 26 | suspend fun insertRef( 27 | realm: Realm, 28 | binding: BindingId, 29 | type: BindingRefType, 30 | artifactId: Long, 31 | sourceFileId: Long, 32 | line: Int, 33 | idInSourceFile: Int 34 | ) 35 | 36 | suspend fun insertDecl( 37 | realm: Realm, 38 | artifactId: Long, 39 | bindingId: BindingId, 40 | binding: String, 41 | sourceFileId: Long?, 42 | includeInTypeSearch: Boolean, 43 | descBytes: ByteArray, 44 | modifiers: Int, 45 | parent: BindingId? 46 | ) 47 | 48 | suspend fun insertLexemes( 49 | set: FullTextSearchSet, 50 | realm: Realm, 51 | artifactId: Long, 52 | sourceFileId: Long, 53 | lexemes: TsVector, 54 | starts: IntArray, 55 | lengths: IntArray 56 | ) 57 | 58 | enum class FullTextSearchSet(@Language(value = "sql", prefix = "select 1 from ") val table: String) { 59 | NORMAL("source_file_lexemes"), 60 | NO_SYMBOLS("source_file_lexemes_no_symbols"), 61 | } 62 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/KeywordHandler.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import at.yawk.javabrowser.IntRangeSet 4 | import at.yawk.javabrowser.Style 5 | import at.yawk.javabrowser.generator.source.BindingVisitor 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | internal object KeywordHandler { 11 | private val normal = listOf( 12 | "abstract", "continue", "for", "new", "switch", "assert", "default", "if", "package", "synchronized", 13 | "boolean", "do", "goto", "private", "this", "break", "double", "implements", "protected", "throw", "byte", 14 | "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", 15 | "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", 16 | "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "true", "false", "null" 17 | ) 18 | private val module = listOf( 19 | "open", "module", "requires", "transitive", "exports", "opens", "to", "uses", "provides", "with" 20 | ) 21 | 22 | private val regex = mapOf( 23 | BindingVisitor.SourceFileType.REGULAR to normal, 24 | BindingVisitor.SourceFileType.MODULE_INFO to normal + module, 25 | BindingVisitor.SourceFileType.PACKAGE_INFO to normal 26 | ).mapValues { "\\b(${it.value.joinToString("|")})\\b".toPattern() } 27 | 28 | private val KEYWORD_STYLE = Style("keyword") 29 | 30 | fun annotateKeywords(annotatedSourceFile: GeneratorSourceFile, 31 | disable: IntRangeSet, 32 | sourceFileType: BindingVisitor.SourceFileType) { 33 | val matcher = regex.getValue(sourceFileType).matcher(annotatedSourceFile.text) 34 | while (matcher.find()) { 35 | if (!disable.contains(matcher.start())) { 36 | annotatedSourceFile.annotate(matcher.start(), matcher.end() - matcher.start(), KEYWORD_STYLE) 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/index.ftl: -------------------------------------------------------------------------------- 1 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.IndexView" --> 2 | <#import "page.ftl" as page> 3 | <#import "fullTextSearchForm.ftl" as ftsf> 4 | <@page.page title="Code Browser" realm='source' newPath=path hasSearch=(path.artifact.stringId == "") narrow=true> 5 |
6 | <#if path.artifact.stringId == ""> 7 |
8 |

This site allows you to explore the source code of the OpenJDK standard library and a selected number of popular maven libraries. Either select an artifact below, or search for a class directly.

9 | 10 | Serving ${siteStatistics.artifactCount} artifacts with ${siteStatistics.sourceFileCount} source files containing ${siteStatistics.classCount} classes, ${siteStatistics.bindingCount} total bindings, ${siteStatistics.referenceCount} references, ${siteStatistics.lexemeCountNoSymbols} words and ${siteStatistics.lexemeCountWithSymbols - siteStatistics.lexemeCountNoSymbols} other tokens. 11 | 12 |

13 | 14 | Contribute on GitHub 15 |

16 |
17 | 18 | 19 | <#if path.artifact.stringId == ""> 20 | <@ftsf.fullTextSearchForm query='' searchArtifact=''/> 21 | 29 | 30 | 31 |

Artifacts

32 |
    33 | <#list path.artifact.flattenedChildren as child> 34 |
  • ${child.stringId}
  • 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /shared/src/main/kotlin/at/yawk/javabrowser/TsQuery.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import java.util.EnumSet 4 | 5 | sealed class TsQuery { 6 | abstract override fun toString(): String 7 | 8 | data class Negation(val query: TsQuery) : TsQuery() { 9 | override fun toString() = "!$query" 10 | } 11 | 12 | data class Disjunction(val operands: List) : TsQuery() { 13 | override fun toString() = operands.joinToString(separator = " | ", prefix = "(", postfix = ")") 14 | } 15 | 16 | data class Conjunction(val operands: List) : TsQuery() { 17 | override fun toString() = operands.joinToString(separator = " & ", prefix = "(", postfix = ")") 18 | } 19 | 20 | data class Phrase(val operands: List, val distance: Int = 0) : TsQuery() { 21 | override fun toString() = operands.joinToString( 22 | separator = " <${distance + 1}> ", prefix = "(", postfix = ")") 23 | } 24 | 25 | data class Term(val term: String, 26 | val weights: Set = EnumSet.allOf(Weight::class.java), 27 | val matchStart: Boolean = false) : TsQuery() { 28 | val matchAllWeights: Boolean 29 | get() = weights.size == 4 30 | 31 | override fun toString(): String { 32 | var suffix: String 33 | if (matchAllWeights && !matchStart) { 34 | suffix = "" 35 | } else { 36 | suffix = ":" 37 | if (!matchAllWeights) { 38 | suffix = weights.joinToString(prefix = ":") { it.value.toString() } 39 | } 40 | if (matchStart) { 41 | suffix += "*" 42 | } 43 | } 44 | // escape the term 45 | return "'" + term.replace("'", "''") + "'" + suffix 46 | } 47 | } 48 | 49 | enum class Weight(val value: Char) { 50 | A('A'), B('B'), C('C'), D('D'); 51 | 52 | companion object { 53 | fun byValue(value: Char) = values().single { it.value == value } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/MultiSemaphore.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | import kotlin.coroutines.Continuation 5 | import kotlin.coroutines.resume 6 | import kotlin.coroutines.suspendCoroutine 7 | 8 | class MultiSemaphore(private var permits: Int = 0) { 9 | private val mutex = Mutex() 10 | private val waiting = ArrayList, Int>>() 11 | 12 | suspend inline fun withPermits(n: Int, f: () -> R): R { 13 | acquire(n) 14 | var tr: Throwable? = null 15 | try { 16 | return f() 17 | } catch (t: Throwable) { 18 | tr = t 19 | throw t 20 | } finally { 21 | try { 22 | release(n) 23 | } catch (t: Throwable) { 24 | if (tr != null) { 25 | tr.addSuppressed(t) 26 | } else { 27 | throw t 28 | } 29 | } 30 | } 31 | } 32 | 33 | suspend fun acquire(n: Int) { 34 | mutex.lock() 35 | if (permits >= n) { 36 | permits -= n 37 | mutex.unlock() 38 | } else { 39 | // wait for permits to become available 40 | return suspendCoroutine { continuation -> 41 | waiting.add(continuation to n) 42 | mutex.unlock() 43 | } 44 | } 45 | } 46 | 47 | suspend fun release(n: Int) { 48 | mutex.lock() 49 | permits += n 50 | val toContinue = ArrayList>() 51 | val iterator = waiting.iterator() 52 | while (iterator.hasNext()) { 53 | val (continuation, required) = iterator.next() 54 | if (permits >= required) { 55 | permits -= required 56 | toContinue.add(continuation) 57 | iterator.remove() 58 | } 59 | } 60 | mutex.unlock() 61 | 62 | for (continuation in toContinue) { 63 | continuation.resume(Unit) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/work/PrepareAndroidWorkerTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.generator.ArtifactConfig 5 | import at.yawk.javabrowser.generator.SourceSetConfig 6 | import kotlinx.coroutines.runBlocking 7 | import org.testng.Assert.assertEquals 8 | import org.testng.Assert.assertNotNull 9 | import org.testng.annotations.Test 10 | import java.net.URL 11 | 12 | class PrepareAndroidWorkerTest { 13 | @Test(groups = ["longRunningDownload"]) 14 | fun test() { 15 | val config = ArtifactConfig.Android( 16 | repos = listOf( 17 | ArtifactConfig.GitRepo( 18 | URL("https://android.googlesource.com/platform/frameworks/base"), 19 | "android-9.0.0_r35" 20 | ), 21 | ArtifactConfig.GitRepo( 22 | URL("https://android.googlesource.com/platform/system/vold"), 23 | "android-9.0.0_r35" 24 | ) 25 | ), 26 | buildTools = URL("https://dl-ssl.google.com/android/repository/build-tools_r28.0.3-linux.zip"), 27 | version = "android-9.0.0_r35", 28 | metadata = ArtifactMetadata() 29 | ) 30 | val worker = PrepareAndroidWorker(TempDirProviderTest) 31 | val artifactId = worker.getArtifactId(config) 32 | assertEquals(artifactId, "android/android-9.0.0_r35") 33 | runBlocking { 34 | worker.prepareArtifact(artifactId, config, 35 | object : PrepareArtifactWorker.PrepareListener { 36 | var metadata: PrepareArtifactWorker.Metadata? = null 37 | 38 | override fun acceptMetadata(metadata: PrepareArtifactWorker.Metadata) { 39 | this.metadata = metadata 40 | } 41 | 42 | override suspend fun compileSourceSet(config: SourceSetConfig) { 43 | assertNotNull(metadata) 44 | } 45 | } 46 | ) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/Alternative.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.Realm 4 | import at.yawk.javabrowser.server.Locations 5 | import at.yawk.javabrowser.server.ParsedPath 6 | import at.yawk.javabrowser.server.VersionComparator 7 | import at.yawk.javabrowser.server.artifact.ArtifactNode 8 | 9 | /** 10 | * Alternate version for a source file, artifact or directory. 11 | */ 12 | data class Alternative( 13 | /** 14 | * The alternative artifact. 15 | */ 16 | val artifact: ArtifactNode, 17 | /** 18 | * The absolute path to the alternative, excluding any hash. 19 | */ 20 | val path: String, 21 | /** 22 | * The complete absolute path to the diff view. 23 | */ 24 | val diffPath: String? 25 | ) { 26 | companion object { 27 | internal fun fromPathPair( 28 | realm: Realm?, 29 | baseline: ParsedPath, 30 | alt: ParsedPath 31 | ): Alternative { 32 | val cmp = VersionComparator.compare(baseline.artifact.stringId, alt.artifact.stringId) 33 | val realmSuffix = 34 | if (realm != null && 35 | ((baseline is ParsedPath.SourceFile && baseline.realmFromExtension != realm) || 36 | (alt is ParsedPath.SourceFile && alt.realmFromExtension != realm)) 37 | ) 38 | "&realm=$realm" 39 | else "" 40 | return Alternative( 41 | alt.artifact, Locations.toPath(alt), when { 42 | cmp > 0 -> Locations.diffPath( 43 | sourceFileNew = Locations.toPath(alt), 44 | sourceFileOld = Locations.toPath(baseline) 45 | ) + realmSuffix 46 | cmp < 0 -> Locations.diffPath( 47 | sourceFileNew = Locations.toPath(baseline), 48 | sourceFileOld = Locations.toPath(alt) 49 | ) + realmSuffix 50 | else -> null 51 | } 52 | ) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/typeReferences.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import org.objectweb.asm.TypePath 4 | import org.objectweb.asm.TypeReference 5 | 6 | internal fun sortToString(typeReference: TypeReference) = when (typeReference.sort) { 7 | TypeReference.CLASS_TYPE_PARAMETER -> "CLASS_TYPE_PARAMETER" 8 | TypeReference.METHOD_TYPE_PARAMETER -> "METHOD_TYPE_PARAMETER" 9 | TypeReference.CLASS_EXTENDS -> "CLASS_EXTENDS" 10 | TypeReference.CLASS_TYPE_PARAMETER_BOUND -> "CLASS_TYPE_PARAMETER_BOUND" 11 | TypeReference.METHOD_TYPE_PARAMETER_BOUND -> "METHOD_TYPE_PARAMETER_BOUND" 12 | TypeReference.FIELD -> "FIELD" 13 | TypeReference.METHOD_RETURN -> "METHOD_RETURN" 14 | TypeReference.METHOD_RECEIVER -> "METHOD_RECEIVER" 15 | TypeReference.METHOD_FORMAL_PARAMETER -> "METHOD_FORMAL_PARAMETER" 16 | TypeReference.THROWS -> "THROWS" 17 | TypeReference.LOCAL_VARIABLE -> "LOCAL_VARIABLE" 18 | TypeReference.RESOURCE_VARIABLE -> "RESOURCE_VARIABLE" 19 | TypeReference.EXCEPTION_PARAMETER -> "EXCEPTION_PARAMETER" 20 | TypeReference.INSTANCEOF -> "INSTANCEOF" 21 | TypeReference.NEW -> "NEW" 22 | TypeReference.CONSTRUCTOR_REFERENCE -> "CONSTRUCTOR_REFERENCE" 23 | TypeReference.METHOD_REFERENCE -> "METHOD_REFERENCE" 24 | TypeReference.CAST -> "CAST" 25 | TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT -> "CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT" 26 | TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT -> "METHOD_INVOCATION_TYPE_ARGUMENT" 27 | TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT -> "CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT" 28 | TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT -> "METHOD_REFERENCE_TYPE_ARGUMENT" 29 | else -> throw AssertionError("unknown sort: ${typeReference.sort}") 30 | } 31 | 32 | internal fun typePathStepToString(step: Int) = when (step) { 33 | TypePath.ARRAY_ELEMENT -> "ARRAY_ELEMENT" 34 | TypePath.INNER_TYPE -> "INNER_TYPE" 35 | TypePath.WILDCARD_BOUND -> "WILDCARD_BOUND" 36 | TypePath.TYPE_ARGUMENT -> "TYPE_ARGUMENT" 37 | else -> throw AssertionError("unknown type path step: $step") 38 | } -------------------------------------------------------------------------------- /server/src/main/resources/assets/search.css: -------------------------------------------------------------------------------- 1 | /* search */ 2 | .search-box, .fts-form { 3 | max-width: 60em; 4 | font-family: "DejaVu Sans", sans-serif; 5 | margin: 1em 0; 6 | } 7 | .search-dialog { 8 | margin: 0 auto; 9 | } 10 | 11 | .search-box input, .fts-form input[type=text] { 12 | width: 100%; 13 | height: 2em; 14 | } 15 | 16 | .search-box li { 17 | list-style: none; 18 | width: 100%; 19 | } 20 | 21 | .search-box ul { 22 | width: 100%; 23 | } 24 | 25 | .search-box li a { 26 | padding: .2em; 27 | display: block; 28 | width: 100%; 29 | } 30 | 31 | .search-box li a:hover { 32 | background: #eee; 33 | } 34 | 35 | .search-dialog { 36 | margin-top: 2em; 37 | border-radius: 2px; 38 | padding: 1em; 39 | } 40 | 41 | .search-dialog > * { 42 | box-sizing: border-box; 43 | } 44 | 45 | .fts-result:first-child { 46 | border: 0; 47 | } 48 | .fts-result { 49 | width: auto; 50 | border-top: 1px solid; 51 | padding: 1em 0; 52 | } 53 | .fts-result code { 54 | margin-bottom: .5em; 55 | padding-bottom: .5em; 56 | border-bottom: 1px solid; 57 | display: block; 58 | } 59 | .fts-result code:last-child { 60 | margin: 0; 61 | border: 0; 62 | padding: 0; 63 | } 64 | 65 | @keyframes rotate { 66 | from { transform: rotate(0deg); } 67 | to { transform: rotate(360deg); } 68 | } 69 | 70 | .search-spinner-wrapper { 71 | width: 100%; 72 | position: relative; 73 | } 74 | 75 | .search-spinner-wrapper .spinner { 76 | display: none; 77 | } 78 | 79 | .search-spinner-wrapper .loading ~ .spinner { 80 | position: absolute; 81 | right: 0; 82 | top: 50%; 83 | display: block; 84 | } 85 | 86 | .declaration-tree .spinner { 87 | margin: .1em; 88 | } 89 | 90 | .spinner { 91 | display: block; 92 | border: solid black; 93 | border-width: 1px 0 0 1px; 94 | width: 1em; 95 | height: 1em; 96 | margin: -.5em .5em 0 0; 97 | border-radius: 50%; 98 | animation: rotate 1s infinite linear; 99 | background: white; 100 | box-sizing: border-box; 101 | } -------------------------------------------------------------------------------- /benchmark-index-plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import collections 4 | 5 | Result = collections.namedtuple("Result", "benchmark chunk_size runtime runtime_error") 6 | 7 | results = [] 8 | 9 | with open("benchmark-index-data-runtime.tsv") as f: 10 | for line in f: 11 | if line.startswith("#"): 12 | continue 13 | if line.strip() == "": 14 | continue 15 | (benchmark, chunk_size, runtime, runtime_error) = line.split() 16 | results.append(Result(benchmark, int(chunk_size), float(runtime), float(runtime_error))) 17 | 18 | memory_by_jumps = { 19 | 0: [], 20 | 1: [], 21 | 2: [], 22 | 3: [], 23 | 4: [], 24 | 5: [], 25 | } 26 | 27 | with open("benchmark-index-data-memory.tsv") as f: 28 | for line in f: 29 | if line.startswith("#"): 30 | continue 31 | if line.strip() == "": 32 | continue 33 | (chunk_size, jumps, memory) = line.split() 34 | # assume ordered by chunk_size 35 | memory_by_jumps[int(jumps)].append(int(memory)) 36 | 37 | import matplotlib.pyplot as plt 38 | import matplotlib.axes 39 | 40 | x = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] 41 | 42 | ax_runtime: matplotlib.axes.Axes 43 | fig, ax_memory = plt.subplots() 44 | 45 | ax_runtime: matplotlib.axes.Axes = ax_memory.twinx() 46 | 47 | ax_memory.set_ylim(0, 7e9) 48 | ax_memory.set_ylabel("memory/B") 49 | bottom = [0 for _ in x] 50 | for jumps in memory_by_jumps: 51 | here = memory_by_jumps[jumps] 52 | ax_memory.bar(x, here, bottom=bottom, width=map(lambda s: s / 2, x)) 53 | bottom = [i + j for i, j in zip(bottom, here)] 54 | 55 | plt.xlim(1, 512) 56 | plt.xlabel("chunk size") 57 | plt.xscale("log", basex=2) 58 | ax_runtime.set_ylim(0.01, 100) 59 | ax_runtime.set_ylabel("runtime/ms") 60 | ax_runtime.set_yscale("log") 61 | for benchmark in set([result.benchmark for result in results]): 62 | def find_y(chunk_size: int): 63 | for result in results: 64 | if result.chunk_size == chunk_size and result.benchmark == benchmark: 65 | return result.runtime 66 | assert False 67 | 68 | 69 | line, = ax_runtime.plot(x, list(map(find_y, x))) 70 | line.set_label(benchmark) 71 | 72 | plt.show() 73 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/AsyncTransaction.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.BindingRefType 5 | import at.yawk.javabrowser.Realm 6 | import at.yawk.javabrowser.TsVector 7 | import kotlinx.coroutines.channels.SendChannel 8 | 9 | class AsyncTransaction( 10 | private val target: SendChannel Unit> 11 | ) : Transaction { 12 | private suspend fun async(task: suspend (Transaction) -> Unit) { 13 | target.send(task) 14 | } 15 | 16 | override suspend fun insertArtifact(id: Long, stringId: String, compilerVersion: Int, metaBytes: ByteArray) = async { 17 | it.insertArtifact(id, stringId, compilerVersion, metaBytes) 18 | } 19 | 20 | override suspend fun insertDependency(from: Long, to: String) = async { 21 | it.insertDependency(from, to) 22 | } 23 | 24 | override suspend fun insertAlias(from: Long, alias: String) = async { 25 | it.insertAlias(from, alias) 26 | } 27 | 28 | override suspend fun insertSourceFile(realm: Realm, artifactId: Long, sourceFileId: Long, hash: Long, path: String, textBytes: ByteArray, annotationBytes: ByteArray) = async { 29 | it.insertSourceFile(realm, artifactId, sourceFileId, hash, path, textBytes, annotationBytes) 30 | } 31 | 32 | override suspend fun insertRef(realm: Realm, binding: BindingId, type: BindingRefType, artifactId: Long, sourceFileId: Long, line: Int, idInSourceFile: Int) = async { 33 | it.insertRef(realm, binding, type, artifactId, sourceFileId, line, idInSourceFile) 34 | } 35 | 36 | override suspend fun insertDecl(realm: Realm, artifactId: Long, bindingId: BindingId, binding: String, sourceFileId: Long?, includeInTypeSearch: Boolean, descBytes: ByteArray, modifiers: Int, parent: BindingId?) = async { 37 | it.insertDecl(realm, artifactId, bindingId, binding, sourceFileId, includeInTypeSearch, descBytes, modifiers, parent) 38 | } 39 | 40 | override suspend fun insertLexemes(set: Transaction.FullTextSearchSet, realm: Realm, artifactId: Long, sourceFileId: Long, lexemes: TsVector, starts: IntArray, lengths: IntArray) = async { 41 | it.insertLexemes(set, realm, artifactId, sourceFileId, lexemes, starts, lengths) 42 | } 43 | } -------------------------------------------------------------------------------- /generator/src/main/resources/at/yawk/javabrowser/generator/DataIndex.sql: -------------------------------------------------------------------------------- 1 | alter table source_file add primary key (realm, artifact_id, source_file_id); 2 | create unique index on source_file (realm, artifact_id, path); 3 | alter table source_file add foreign key (artifact_id) references artifact on delete cascade; 4 | alter table source_file_lexemes add foreign key (realm, artifact_id, source_file_id) references source_file on delete cascade; 5 | alter table source_file_lexemes_no_symbols add foreign key (realm, artifact_id, source_file_id) references source_file on delete cascade; 6 | create index on source_file_lexemes using gin(lexemes); 7 | create index on source_file_lexemes_no_symbols using gin(lexemes); 8 | 9 | alter table binding add primary key (realm, binding_id, artifact_id) include (source_file_id); 10 | alter table binding add foreign key (realm, artifact_id, source_file_id) references source_file on delete cascade; 11 | -- sometimes bindings may be inserted before their parent, so we can't have this foreign key 12 | -- alter table binding add foreign key (realm, parent, artifact_id) references binding; 13 | 14 | create index on _binding0 (artifact_id, parent); 15 | create index on _binding1 (artifact_id, parent); 16 | 17 | alter table binding_reference add primary key (realm, source_artifact_id, source_file_id, source_file_ref_id); 18 | alter table binding_reference add foreign key (realm, source_artifact_id, source_file_id) references source_file on delete cascade; 19 | create index on _binding_reference0 (target, type, source_artifact_id); 20 | create index on _binding_reference1 (target, type, source_artifact_id); 21 | -- Due to a planner limitation ( https://commitfest.postgresql.org/15/1124/ ), postgres is currently unable to 22 | -- sort by (type, sourceArtifactId, sourceFile, sourceFileId) using the index over 23 | -- (targetBinding, type, sourceArtifactId). For this reason we add this new index. 24 | create index on _binding_reference0 (target, type, source_artifact_id, source_file_id, source_file_ref_id); 25 | create index on _binding_reference1 (target, type, source_artifact_id, source_file_id, source_file_ref_id); 26 | 27 | create unique index on binding_reference_count_view (target, type, source_artifact_id); 28 | --create unique index on packages_view (artifact_id, name); 29 | --create unique index on type_count_by_depth_view (artifact_id, package, depth); 30 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/bytecode/testCodegen.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.Realm 5 | import at.yawk.javabrowser.Tokenizer 6 | import at.yawk.javabrowser.generator.GeneratorSourceFile 7 | import at.yawk.javabrowser.generator.SourceSetConfig 8 | import at.yawk.javabrowser.generator.source.Printer 9 | import at.yawk.javabrowser.generator.source.SourceFileParser 10 | import com.google.common.io.MoreFiles 11 | import kotlinx.coroutines.runBlocking 12 | import org.intellij.lang.annotations.Language 13 | import org.objectweb.asm.ClassReader 14 | import java.nio.file.Files 15 | import java.nio.file.Path 16 | 17 | fun testHashBinding(binding: String) = BindingId(binding.hashCode().toLong()) 18 | 19 | fun getOutput( 20 | @Language("java") code: String, 21 | filter: (Path) -> Boolean = { true }, 22 | visitor: (BytecodePrinter, ClassReader) -> Unit 23 | ): String { 24 | val output = BytecodePrinter(::testHashBinding) 25 | 26 | val tmp = Files.createTempDirectory("MethodPrinterTest") 27 | try { 28 | Files.write(tmp.resolve("Main.java"), code.toByteArray()) 29 | 30 | val out = tmp.resolve("out") 31 | Files.createDirectory(out) 32 | val sourceFileParser = SourceFileParser(object : Printer { 33 | override suspend fun addSourceFile(path: String, 34 | sourceFile: GeneratorSourceFile, 35 | tokens: List, 36 | realm: Realm) { 37 | } 38 | 39 | override fun hashBinding(binding: String) = testHashBinding(binding) 40 | }, SourceSetConfig("MethodPrinterTest", tmp, outputClassesTo = out, dependencies = emptyList())) 41 | runBlocking { 42 | sourceFileParser.compile() 43 | } 44 | Files.newDirectoryStream(out).use { classes -> 45 | for (cl in classes.filter(filter)) { 46 | val classReader = ClassReader(Files.readAllBytes(cl)) 47 | visitor(output, classReader) 48 | } 49 | } 50 | } finally { 51 | @Suppress("UnstableApiUsage") 52 | MoreFiles.deleteRecursively(tmp) 53 | } 54 | 55 | return output.finishString().trimIndent() 56 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ArtifactIndex.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.artifact.ArtifactNode 4 | import org.eclipse.collections.api.map.primitive.LongObjectMap 5 | import org.jdbi.v3.core.Handle 6 | import org.jdbi.v3.core.Jdbi 7 | import java.util.NavigableMap 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * @author yawkat 13 | */ 14 | @Singleton 15 | class ArtifactIndex @Inject constructor( 16 | artifactUpdater: ArtifactUpdater, 17 | private val dbi: Jdbi 18 | ) { 19 | private var rootArtifact = fetch() 20 | 21 | init { 22 | artifactUpdater.addInvalidationListener(runAtStart = false) { rootArtifact = fetch() } 23 | } 24 | 25 | private fun fetch(): ArtifactNode { 26 | return dbi.inTransaction { conn: Handle -> 27 | ArtifactNode.build(conn.select("select artifact_id, string_id from artifact").mapToMap().toList().map { 28 | ArtifactNode.Prototype(dbId = (it["artifact_id"] as Number).toLong(), stringId = it["string_id"] as String) 29 | }) 30 | } 31 | } 32 | 33 | val allArtifactsByStringId: NavigableMap 34 | get() = rootArtifact.allNodesByStringId 35 | 36 | val allArtifactsByDbId: LongObjectMap 37 | get() = rootArtifact.allNodesByDbId 38 | 39 | val leafArtifacts: Collection 40 | get() = allArtifactsByDbId.values() 41 | 42 | fun parse(rawPath: String): ParseResult { 43 | val path = rawPath.removePrefix("/").removeSuffix("/") 44 | val pathParts = if (path.isEmpty()) emptyList() else path.split('/') 45 | 46 | var node = rootArtifact 47 | for ((i, pathPart) in pathParts.withIndex()) { 48 | val child = node.children[pathPart] 49 | if (child != null) { 50 | node = child 51 | } else { 52 | val sourceFileParts = pathParts.subList(i, pathParts.size) 53 | val sourceFilePath = sourceFileParts.joinToString("/") 54 | return ParseResult(node, sourceFilePath) 55 | } 56 | } 57 | return ParseResult(node, null) 58 | } 59 | 60 | class ParseResult( 61 | val node: ArtifactNode, 62 | val remainingPath: String? 63 | ) 64 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/ActorAsyncTransactionProvider.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.generator.CoroutineCloseable 4 | import at.yawk.javabrowser.generator.use 5 | import kotlinx.coroutines.CompletableDeferred 6 | import kotlinx.coroutines.CoroutineName 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.channels.actor 9 | import kotlinx.coroutines.withContext 10 | 11 | /** 12 | * [TransactionProvider] implementation that performs DB operations on the [workerScope]. Ops go through a queue of 13 | * maximum size [actorQueueCapacity] for backpressure. 14 | * 15 | * This can be used to limit concurrent db *operations*, though there still may be more concurrent *transactions*. 16 | */ 17 | class ActorAsyncTransactionProvider( 18 | private val delegate: TransactionProvider, 19 | private val workerScope: CoroutineScope, 20 | private val actorQueueCapacity: Int = 16 21 | ) : TransactionProvider { 22 | override suspend fun claimArtifactId() = withContext(workerScope.coroutineContext) { 23 | delegate.claimArtifactId() 24 | } 25 | 26 | override suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) { 27 | delegate.withArtifactTransaction(artifactId) { tx -> 28 | ForArtifact(tx, artifactId).use { 29 | task(it.transaction) 30 | } 31 | } 32 | } 33 | 34 | private inner class ForArtifact( 35 | tx: Transaction, 36 | artifactStringId: String 37 | ) : CoroutineCloseable { 38 | val completion = CompletableDeferred() 39 | 40 | @Suppress("EXPERIMENTAL_API_USAGE") 41 | private val transactionChannel = workerScope.actor Unit>( 42 | CoroutineName("transactionChannel: $artifactStringId"), 43 | onCompletion = { 44 | if (it == null) completion.complete(Unit) 45 | else completion.completeExceptionally(it) 46 | }, 47 | capacity = actorQueueCapacity 48 | ) { 49 | for (task in this) { 50 | task(tx) 51 | } 52 | } 53 | 54 | val transaction = AsyncTransaction(transactionChannel) 55 | 56 | override suspend fun close() { 57 | transactionChannel.close() 58 | completion.await() 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/db/TestTransaction.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.BindingRefType 5 | import at.yawk.javabrowser.Realm 6 | import at.yawk.javabrowser.TsVector 7 | import org.eclipse.collections.impl.factory.primitive.LongObjectMaps 8 | 9 | open class TestTransaction : Transaction { 10 | private val visitedBindingIds = LongObjectMaps.mutable.empty() 11 | 12 | open suspend fun onAnyTask() { 13 | } 14 | 15 | override suspend fun insertArtifact(id: Long, stringId: String, compilerVersion: Int, metaBytes: ByteArray) { 16 | onAnyTask() 17 | } 18 | 19 | override suspend fun insertDependency(from: Long, to: String) { 20 | onAnyTask() 21 | } 22 | 23 | override suspend fun insertAlias(from: Long, alias: String) { 24 | onAnyTask() 25 | } 26 | 27 | override suspend fun insertSourceFile( 28 | realm: Realm, 29 | artifactId: Long, 30 | sourceFileId: Long, 31 | hash: Long, 32 | path: String, 33 | textBytes: ByteArray, 34 | annotationBytes: ByteArray 35 | ) { 36 | onAnyTask() 37 | } 38 | 39 | override suspend fun insertRef( 40 | realm: Realm, 41 | binding: BindingId, 42 | type: BindingRefType, 43 | artifactId: Long, 44 | sourceFileId: Long, 45 | line: Int, 46 | idInSourceFile: Int 47 | ) { 48 | onAnyTask() 49 | } 50 | 51 | override suspend fun insertDecl( 52 | realm: Realm, 53 | artifactId: Long, 54 | bindingId: BindingId, 55 | binding: String, 56 | sourceFileId: Long?, 57 | includeInTypeSearch: Boolean, 58 | descBytes: ByteArray, 59 | modifiers: Int, 60 | parent: BindingId? 61 | ) { 62 | val old = visitedBindingIds.put(bindingId.hash, binding) 63 | if (old != null) { 64 | throw IllegalArgumentException("Duplicate binding ID $old $binding") 65 | } 66 | 67 | onAnyTask() 68 | } 69 | 70 | override suspend fun insertLexemes( 71 | set: Transaction.FullTextSearchSet, 72 | realm: Realm, 73 | artifactId: Long, 74 | sourceFileId: Long, 75 | lexemes: TsVector, 76 | starts: IntArray, 77 | lengths: IntArray 78 | ) { 79 | onAnyTask() 80 | } 81 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/FullTextSearchResultView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.Realm 4 | import at.yawk.javabrowser.server.BindingResolver 5 | import at.yawk.javabrowser.server.ParsedPath 6 | import at.yawk.javabrowser.server.SourceFilePrinter 7 | import at.yawk.javabrowser.server.artifact.ArtifactNode 8 | import freemarker.core.Environment 9 | import freemarker.template.TemplateDirectiveBody 10 | import freemarker.template.TemplateDirectiveModel 11 | import freemarker.template.TemplateModel 12 | import java.net.URI 13 | 14 | /** 15 | * @author yawkat 16 | */ 17 | private const val CONTEXT_LINES = 2 18 | 19 | class FullTextSearchResultView( 20 | val query: String, 21 | val searchRealm: Realm?, 22 | val searchArtifact: ArtifactNode?, 23 | val results: Iterator 24 | ) : View("fullTextSearchResultView.ftl") { 25 | val path = searchArtifact?.let { ParsedPath.LeafArtifact(searchArtifact) } 26 | 27 | class SourceFileResult( 28 | private val bindingResolver: BindingResolver, 29 | 30 | val realm: Realm, 31 | val artifactId: String, 32 | val path: String, 33 | private val classpath: List, 34 | partial: SourceFilePrinter.Partial 35 | ) { 36 | val renderer = partial.createRenderer(CONTEXT_LINES, CONTEXT_LINES) 37 | 38 | val hasMore: Boolean 39 | get() = renderer.hasMore() 40 | 41 | val renderNextRegionDirective = PrinterDirective() 42 | 43 | inner class PrinterDirective : TemplateDirectiveModel { 44 | override fun execute(env: Environment, 45 | params: MutableMap, 46 | loopVars: Array, 47 | body: TemplateDirectiveBody?) { 48 | val emitter = HtmlEmitter( 49 | bindingResolver, 50 | mapOf(SourceFilePrinter.Scope.NORMAL to HtmlEmitter.ScopeInfo(realm, artifactId, classpath)), 51 | env.out, 52 | 53 | hasOverlay = false, 54 | referenceThisUrl = false, 55 | renderJavadoc = false, 56 | ownUri = URI.create("/$artifactId/$path") 57 | ) 58 | renderer.renderNextRegion(emitter) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/FullUpdateStrategy.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.generator.COMPILER_VERSION 4 | import at.yawk.javabrowser.generator.GeneratorSchema 5 | import org.jdbi.v3.core.Handle 6 | import org.jdbi.v3.core.Jdbi 7 | import org.slf4j.LoggerFactory 8 | 9 | private val log = LoggerFactory.getLogger(FullUpdateStrategy::class.java) 10 | 11 | class FullUpdateStrategy(dbi: Jdbi) : UpdateStrategy("wip", dbi) { 12 | override fun prepare() { 13 | dbi.inTransaction { conn: Handle -> 14 | var hasWipSchema = hasSchema(conn) 15 | if (hasWipSchema) { 16 | val hasOutdated = conn.select("select 1 from wip.artifact where last_compile_version < ?", COMPILER_VERSION).mapToMap().any() 17 | if (hasOutdated) { 18 | log.info("Found outdated artifacts in existing `wip` schema. Dropping `wip` schema to start anew.") 19 | conn.createUpdate("drop schema wip cascade").execute() 20 | hasWipSchema = false 21 | } 22 | } 23 | if (!hasWipSchema) { 24 | log.info("Creating `wip` schema") 25 | conn.createUpdate("create schema wip").execute() 26 | setConnectionSchema(conn) 27 | GeneratorSchema(conn).createSchema() 28 | } else { 29 | log.info("Running with existing `wip` schema") 30 | } 31 | } 32 | } 33 | 34 | override fun finish(allArtifacts: Collection) { 35 | dbi.inTransaction { conn -> 36 | setConnectionSchema(conn) 37 | 38 | log.info("Creating indices") 39 | GeneratorSchema(conn).createIndices() 40 | GeneratorSchema(conn).updateViews(concurrent = false) 41 | 42 | log.info("Replacing schema") 43 | conn.createUpdate("drop schema data cascade").execute() 44 | conn.createUpdate("alter schema wip rename to data").execute() 45 | 46 | for (artifact in allArtifacts) { 47 | notifyUpdate(conn, artifact) 48 | } 49 | } 50 | } 51 | 52 | override suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) { 53 | dbi.inTransactionSuspend { conn -> 54 | setConnectionSchema(conn) 55 | val tx = DirectTransaction(conn) 56 | task(tx) 57 | tx.flush() 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/BindingResolver.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.Realm 5 | import com.google.common.cache.CacheBuilder 6 | import com.google.common.cache.CacheLoader 7 | import org.jdbi.v3.core.Handle 8 | import org.jdbi.v3.core.Jdbi 9 | import java.net.URI 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | /** 14 | * @author yawkat 15 | */ 16 | @Singleton 17 | class BindingResolver @Inject constructor( 18 | artifactUpdater: ArtifactUpdater, 19 | private val dbi: Jdbi, 20 | private val artifactIndex: ArtifactIndex 21 | ) { 22 | private val caches = Realm.values().associate { 23 | it to CacheBuilder.newBuilder() 24 | .softValues() 25 | .build>(CacheLoader.from { binding -> resolveBinding0(it, binding!!) }) 26 | } 27 | 28 | init { 29 | // too hard to just invalidate relevant bindings 30 | artifactUpdater.addInvalidationListener(runAtStart = false) { caches.values.forEach { it.invalidateAll() } } 31 | } 32 | 33 | private fun resolveBinding0(realm: Realm, binding: BindingId): List { 34 | return dbi.inTransaction, Exception> { conn: Handle -> 35 | conn.createQuery("select artifact_id, source_file.path, binding.binding from binding natural join source_file where realm = ? and binding_id = ?") 36 | .bind(0, realm.id) 37 | .bind(1, binding.hash) 38 | .map { r, _, _ -> BindingLocation(artifactIndex.allArtifactsByDbId[r.getLong(1)].stringId, r.getString(2), r.getString(3)) } 39 | .list() 40 | } 41 | } 42 | 43 | fun resolveBinding(realm: Realm, fromArtifacts: List, binding: BindingId): List { 44 | val candidates = caches.getValue(realm)[binding] 45 | // in order of artifact priority 46 | for (artifact in fromArtifacts) { 47 | for (candidate in candidates) { 48 | if (candidate.artifact == artifact) { 49 | return listOf(candidate.uri) 50 | } 51 | } 52 | } 53 | return candidates.map { it.uri } 54 | } 55 | 56 | private data class BindingLocation( 57 | val artifact: String, 58 | val sourceFile: String, 59 | val binding: String 60 | ) { 61 | val uri = Locations.location(artifact, sourceFile, Locations.bindingHash(binding)) 62 | } 63 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/ReferenceDetailView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.BindingRefType 4 | import at.yawk.javabrowser.Realm 5 | import at.yawk.javabrowser.server.Locations 6 | import at.yawk.javabrowser.server.ParsedPath 7 | import at.yawk.javabrowser.server.VersionComparator 8 | import at.yawk.javabrowser.server.artifact.ArtifactNode 9 | import com.google.common.collect.Table 10 | import java.net.URI 11 | 12 | /** 13 | * @author yawkat 14 | */ 15 | data class ReferenceDetailView( 16 | val realm: Realm, 17 | val targetBinding: String, 18 | val baseUri: URI, 19 | val type: BindingRefType?, 20 | val sourceArtifactId: ArtifactNode?, 21 | 22 | val countTable: Table, 23 | val countsByType: Map, 24 | val countsByArtifact: Map, 25 | val totalCount: Int, 26 | 27 | val resultLimit: Int?, 28 | val hitResultLimit: Boolean, 29 | val totalCountInSelection: Int, 30 | 31 | /** null elements are ignored */ 32 | val results: Iterator 33 | ) : View("referenceDetail.ftl") { 34 | val artifactPath = sourceArtifactId?.let { ParsedPath.LeafArtifact(it) } 35 | 36 | val artifacts: List = countsByArtifact.keys.sortedWith(VersionComparator) 37 | 38 | class TypeListing( 39 | val type: BindingRefType, 40 | /** null elements are ignored */ 41 | val artifacts: Iterator 42 | ) { 43 | override fun toString() = "ArtifactListing(type=$type, artifacts=...)" 44 | } 45 | 46 | class ArtifactListing( 47 | val artifactId: String, 48 | /** null elements are ignored */ 49 | val sourceFiles: Iterator 50 | ) { 51 | override fun toString() = "ArtifactListing(artifactId=$artifactId, sourceFiles=...)" 52 | } 53 | 54 | data class SourceFileListing( 55 | val sourceFile: String, 56 | val items: List 57 | ) 58 | 59 | class ReferenceListing( 60 | sourceArtifactStringId: String, 61 | sourceFile: String, 62 | val sourceFileLine: Int, 63 | sourceFileRefId: Int 64 | ) { 65 | val sourceLocation = Locations.location(sourceArtifactStringId, sourceFile, "#ref-$sourceFileRefId") 66 | 67 | override fun toString(): String { 68 | return "ReferenceListing(sourceFileLine=$sourceFileLine, sourceLocation=$sourceLocation)" 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/fields.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingDecl 4 | import at.yawk.javabrowser.BindingRefType 5 | import org.objectweb.asm.Type 6 | import org.objectweb.asm.tree.FieldNode 7 | 8 | internal fun printField(printer: BytecodePrinter, owner: Type, field: FieldNode) { 9 | val type = Type.getType(field.desc) 10 | printer.indent(1) 11 | printer.printSourceModifiers(field.access, Flag.Target.FIELD, trailingSpace = true) 12 | if (field.signature != null) { 13 | printer.printJavaSignature(field.signature, access = field.access, isTypeSignature = true) 14 | } else { 15 | printer.appendJavaName(type, BindingRefType.FIELD_TYPE) 16 | } 17 | printer.append(' ') 18 | val binding = BytecodeBindings.toStringField(owner, field.name, type) 19 | val id = printer.hashBinding(binding) 20 | printer.annotate(BindingDecl( 21 | id = id, 22 | binding = binding, 23 | description = BindingDecl.Description.Field( 24 | name = field.name, 25 | typeBinding = typeDescription(printer, type) 26 | ), 27 | modifiers = asmAccessToSourceAnnotation(field.access), 28 | parent = printer.hashBinding(BytecodeBindings.toStringClass(owner)), 29 | superBindings = emptyList(), 30 | corresponding = printer.getCorresponding(id) 31 | )) { 32 | printer.append(field.name) 33 | } 34 | printer.append(";\n") 35 | 36 | printer.indent(2) 37 | printer.append("descriptor: ").appendDescriptor(type, BindingRefType.FIELD_TYPE, duplicate = true).append('\n') 38 | printer.indent(2) 39 | printer.printFlags(field.access, Flag.Target.FIELD) 40 | if (field.signature != null) { 41 | printer.indent(2) 42 | printer.append("Signature: ").appendGenericSignature(field.signature).append('\n') 43 | } 44 | 45 | if (field.value != null) { 46 | printer.indent(2) 47 | printer.append("ConstantValue: ").appendConstant(field.value) 48 | printer.append('\n') 49 | } 50 | 51 | printer.printAnnotations("RuntimeVisibleAnnotations", field.visibleAnnotations) 52 | printer.printAnnotations("RuntimeInvisibleAnnotations", field.invisibleAnnotations) 53 | 54 | printer.printTypeAnnotations("RuntimeVisibleTypeAnnotations", field.visibleTypeAnnotations ?: emptyList()) 55 | printer.printTypeAnnotations("RuntimeInvisibleTypeAnnotations", field.invisibleTypeAnnotations ?: emptyList()) 56 | 57 | // no unknown attrs 58 | require(field.attrs == null) 59 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/DeclarationNode.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.BindingDecl 4 | import at.yawk.javabrowser.BindingId 5 | import at.yawk.javabrowser.Realm 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | data class DeclarationNode( 11 | val realm: Realm, 12 | val artifactId: String, 13 | val parent: BindingId?, 14 | val bindingId: BindingId, 15 | val binding: String, 16 | val description: BindingDecl.Description, 17 | val modifiers: Int, 18 | 19 | /** 20 | * Source file path to this binding, including potential diff query parameter, but excluding the hash to the 21 | * specific binding. 22 | */ 23 | val fullSourceFilePath: String? = null, 24 | 25 | /** 26 | * Once an element has been hasNext'd, previous items may become inaccessible. If this is `null`, children 27 | * should be lazy-loaded. 28 | */ 29 | val children: Iterator?, 30 | 31 | val diffResult: DiffResult? = null 32 | ) { 33 | companion object { 34 | /** 35 | * The comparator used for display ordering. Have to be careful here to preserve source file order for bindings 36 | * that reside in .java files, but at the same time keep a total order for items at the package level. 37 | */ 38 | val DISPLAY_ORDER = Comparator.comparing { it.realm } 39 | .thenComparing { it.artifactId } 40 | .thenComparing { 41 | if (it.description is BindingDecl.Description.Package) it.binding 42 | else it.fullSourceFilePath ?: it.binding 43 | }!! 44 | } 45 | 46 | val kind: Kind 47 | get() = when (description) { 48 | is BindingDecl.Description.Type -> Kind.TYPE 49 | is BindingDecl.Description.Lambda -> Kind.LAMBDA 50 | is BindingDecl.Description.Initializer -> Kind.INITIALIZER 51 | is BindingDecl.Description.Method -> Kind.METHOD 52 | is BindingDecl.Description.Field -> Kind.FIELD 53 | is BindingDecl.Description.Package -> Kind.PACKAGE 54 | } 55 | 56 | val deprecated: Boolean 57 | get() = (modifiers and BindingDecl.MODIFIER_DEPRECATED) != 0 58 | 59 | enum class DiffResult { 60 | UNCHANGED, 61 | CHANGED_INTERNALLY, 62 | INSERTION, 63 | DELETION, 64 | } 65 | 66 | enum class Kind { 67 | PACKAGE, 68 | 69 | TYPE, 70 | LAMBDA, 71 | METHOD, 72 | FIELD, 73 | INITIALIZER 74 | } 75 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/SiteStatisticsService.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.view.SiteStatistics 4 | import org.jdbi.v3.core.Jdbi 5 | import org.jdbi.v3.sqlobject.statement.SqlQuery 6 | import org.slf4j.LoggerFactory 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | /** 11 | * @author yawkat 12 | */ 13 | private val log = LoggerFactory.getLogger(SiteStatisticsService::class.java) 14 | 15 | @Singleton 16 | class SiteStatisticsService @Inject constructor( 17 | private val dbi: Jdbi, 18 | artifactUpdater: ArtifactUpdater 19 | ) { 20 | private val debouncer = Debouncer() 21 | 22 | // loading takes a while, so initialize with 0. 23 | var statistics: SiteStatistics = SiteStatistics(0, 0, 0, 0, 0, 0, 0) 24 | private set 25 | 26 | init { 27 | artifactUpdater.addInvalidationListener(runAtStart = true) { 28 | debouncer.requestRun { statistics = loadStatistics() } 29 | } 30 | } 31 | 32 | private fun loadStatistics(): SiteStatistics = dbi.withHandle { 33 | log.info("Loading site statistics") 34 | val dao = it.attach(Dao::class.java) 35 | val siteStatistics = SiteStatistics( 36 | artifactCount = dao.getArtifactCount(), 37 | bindingCount = dao.getBindingCount(), 38 | classCount = dao.getClassCount(), 39 | referenceCount = dao.getReferenceCount(), 40 | sourceFileCount = dao.getSourceFileCount(), 41 | lexemeCountNoSymbols = dao.getLexemeCountNoSymbols(), 42 | lexemeCountWithSymbols = dao.getLexemeCountWithSymbols() 43 | ) 44 | log.info("Loaded site statistics: {}", siteStatistics) 45 | siteStatistics 46 | } 47 | 48 | interface Dao { 49 | @SqlQuery("select count(*) from artifact") 50 | fun getArtifactCount(): Long 51 | 52 | @SqlQuery("select count(*) from source_file") 53 | fun getSourceFileCount(): Long 54 | 55 | @SqlQuery("select count(*) from binding") 56 | fun getBindingCount(): Long 57 | 58 | @SqlQuery("select count(*) from binding where include_in_type_search") 59 | fun getClassCount(): Long 60 | 61 | @SqlQuery("select count(*) from binding_reference") 62 | fun getReferenceCount(): Long 63 | 64 | @SqlQuery("select sum(array_length(starts, 1)) from source_file_lexemes_no_symbols") 65 | fun getLexemeCountNoSymbols(): Long 66 | 67 | @SqlQuery("select sum(array_length(starts, 1)) from source_file_lexemes") 68 | fun getLexemeCountWithSymbols(): Long 69 | } 70 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/db/UpdateStrategy.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import at.yawk.javabrowser.generator.COMPILER_VERSION 4 | import org.intellij.lang.annotations.Language 5 | import org.jdbi.v3.core.Handle 6 | import org.jdbi.v3.core.Jdbi 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | internal suspend fun Jdbi.inTransactionSuspend(task: suspend (Handle) -> R): R { 10 | return open().use { handle -> 11 | val failed = AtomicBoolean(false) 12 | val r: R 13 | try { 14 | handle.begin() 15 | r = task(handle) 16 | if (!failed.get()) { 17 | handle.commit() 18 | } 19 | } catch (e: Exception) { 20 | try { 21 | handle.rollback() 22 | } catch (rol: Exception) { 23 | e.addSuppressed(rol) 24 | } 25 | throw e 26 | } 27 | if (failed.get()) { 28 | handle.rollback() 29 | throw Exception("Set to rollback only") 30 | } 31 | r 32 | } 33 | } 34 | 35 | abstract class UpdateStrategy( 36 | @Language(value = "sql", prefix = "set search_path = ") private val workSchema: String, 37 | protected val dbi: Jdbi 38 | ) : TransactionProvider { 39 | fun hasSchema() = dbi.inTransaction { conn -> hasSchema(conn) } 40 | 41 | override suspend fun claimArtifactId(): Long = dbi.inTransaction { conn -> 42 | setConnectionSchema(conn) 43 | (conn.select("select nextval('artifact_id_sequence')").mapToMap().single()["nextval"] as Number).toLong() 44 | } 45 | 46 | protected fun hasSchema(conn: Handle) = 47 | conn.select( 48 | "select 1 from information_schema.tables where table_schema = ? and table_name = 'artifact'", 49 | workSchema 50 | ).mapToMap().any() 51 | 52 | protected fun setConnectionSchema(conn: Handle) { 53 | conn.createUpdate("set search_path = $workSchema").execute() 54 | } 55 | 56 | protected fun notifyUpdate(conn: Handle, artifactStringId: String) { 57 | conn.createUpdate("select pg_notify('artifact', ?)").bind(0, artifactStringId).execute() 58 | } 59 | 60 | fun listUpToDate(): List { 61 | return dbi.inTransaction, Exception> { conn: Handle -> 62 | setConnectionSchema(conn) 63 | conn.select("select string_id from artifact where last_compile_version >= ?", COMPILER_VERSION) 64 | .mapToMap().toList() 65 | .map { it["string_id"] as String } 66 | } 67 | } 68 | 69 | abstract fun prepare() 70 | 71 | abstract fun finish(allArtifacts: Collection) 72 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/GeneratorSourceFile.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import at.yawk.javabrowser.BindingRef 4 | import at.yawk.javabrowser.BindingRefType 5 | import at.yawk.javabrowser.PositionedAnnotation 6 | import at.yawk.javabrowser.SourceAnnotation 7 | import at.yawk.javabrowser.Style 8 | 9 | /** 10 | * @author yawkat 11 | */ 12 | class GeneratorSourceFile( 13 | val pkg: String?, 14 | val text: String, 15 | @Suppress("MemberVisibilityCanBePrivate") val entries: MutableList = ArrayList() 16 | ) { 17 | fun annotate(start: Int, length: Int, annotation: SourceAnnotation) { 18 | entries.add(PositionedAnnotation(start, length, annotation)) 19 | } 20 | 21 | fun bake() { 22 | entries.sortWith(Comparator 23 | .comparingInt { it: PositionedAnnotation -> it.start } 24 | // first 0-length items, then the longest items 25 | // this avoids unnecessary nesting 26 | .thenComparingInt { it -> if (it.length == 0) Int.MIN_VALUE else it.length.inv() }) 27 | 28 | // try to merge entries that affect the same text 29 | var i = 0 30 | while (i < entries.size) { 31 | val head = entries[i] 32 | var j = i + 1 33 | var merged: SourceAnnotation? = null 34 | while (merged == null && j < entries.size && 35 | entries[j].start == head.start && entries[j].length == head.length) { 36 | merged = tryMerge(head.annotation, entries[j++].annotation) 37 | } 38 | if (merged == null) { 39 | i++ 40 | } else { 41 | entries.removeAt(j - 1) 42 | entries[i] = head.copy(annotation = merged) 43 | } 44 | } 45 | } 46 | 47 | private fun tryMerge(a: SourceAnnotation, b: SourceAnnotation): SourceAnnotation? { 48 | if (a is Style && b is Style) { 49 | return Style(a.styleClass + b.styleClass) 50 | } 51 | if (a is BindingRef && b is BindingRef) { 52 | // one method can override multiple supers 53 | if (a.type == BindingRefType.SUPER_METHOD && b.type == BindingRefType.SUPER_METHOD) return null 54 | if (a.type == BindingRefType.SUPER_TYPE && b.type == BindingRefType.SUPER_TYPE) return null 55 | // lambdas have a SUPER_TYPE and SUPER_METHOD ref on the same node 56 | if ((a.type == BindingRefType.SUPER_TYPE && b.type == BindingRefType.SUPER_METHOD) || 57 | (b.type == BindingRefType.SUPER_TYPE && a.type == BindingRefType.SUPER_METHOD)) return null 58 | throw RuntimeException("Duplicate ref: $a / $b") 59 | } 60 | return null 61 | } 62 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/view/SourceFileView.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.view 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.Realm 5 | import at.yawk.javabrowser.server.BindingResolver 6 | import at.yawk.javabrowser.server.ParsedPath 7 | import at.yawk.javabrowser.server.ServerSourceFile 8 | import at.yawk.javabrowser.server.SourceFilePrinter 9 | import freemarker.core.Environment 10 | import freemarker.template.TemplateDirectiveBody 11 | import freemarker.template.TemplateDirectiveModel 12 | import freemarker.template.TemplateModel 13 | 14 | /** 15 | * @author yawkat 16 | */ 17 | @Suppress("unused") 18 | class SourceFileView( 19 | val newInfo: FileInfo, 20 | val oldInfo: FileInfo?, 21 | val alternatives: List, 22 | val artifactMetadata: ArtifactMetadata, 23 | val declarations: Iterator, 24 | 25 | private val bindingResolver: BindingResolver 26 | ) : View("source-file.ftl") { 27 | class FileInfo( 28 | val realm: Realm, 29 | val sourceFile: ServerSourceFile, 30 | val classpath: List, 31 | val sourceFilePath: ParsedPath.SourceFile 32 | ) 33 | 34 | val diff = oldInfo?.let { SourceFilePrinter.Diff(newInfo.sourceFile, it.sourceFile) } 35 | 36 | val printerDirective = PrinterDirective() 37 | 38 | inner class PrinterDirective : TemplateDirectiveModel { 39 | override fun execute(env: Environment, 40 | params: MutableMap, 41 | loopVars: Array, 42 | body: TemplateDirectiveBody?) { 43 | val newScopeInfo = 44 | HtmlEmitter.ScopeInfo(newInfo.realm, newInfo.sourceFilePath.artifact.stringId, newInfo.classpath) 45 | val emitter = HtmlEmitter( 46 | bindingResolver, 47 | if (oldInfo != null) 48 | mapOf( 49 | SourceFilePrinter.Scope.OLD to 50 | HtmlEmitter.ScopeInfo( 51 | oldInfo.realm, 52 | oldInfo.sourceFilePath.artifact.stringId, 53 | oldInfo.classpath 54 | ), 55 | SourceFilePrinter.Scope.NEW to newScopeInfo 56 | ) 57 | else 58 | mapOf(SourceFilePrinter.Scope.NORMAL to newScopeInfo), 59 | env.out, 60 | 61 | hasOverlay = true, 62 | referenceThisUrl = true, 63 | renderJavadoc = diff == null 64 | ) 65 | if (diff != null) { 66 | diff.toHtml(emitter) 67 | } else { 68 | SourceFilePrinter.toHtmlSingle(emitter, newInfo.sourceFile) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/source/Bindings.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.source 2 | 3 | import org.eclipse.jdt.core.dom.IBinding 4 | import org.eclipse.jdt.core.dom.IMethodBinding 5 | import org.eclipse.jdt.core.dom.IPackageBinding 6 | import org.eclipse.jdt.core.dom.ITypeBinding 7 | import org.eclipse.jdt.core.dom.IVariableBinding 8 | 9 | private val LAMBDA_METHOD_CLASS = Class.forName("org.eclipse.jdt.core.dom.MethodBinding\$LambdaMethod") 10 | private val LAMBDA_METHOD_IMPL_FIELD = LAMBDA_METHOD_CLASS.declaredFields.single { it.name == "implementation" }.also { 11 | it.isAccessible = true 12 | } 13 | 14 | /** 15 | * [org.eclipse.jdt.core.dom.MethodBinding.LambdaMethod.implementation] 16 | */ 17 | internal fun unsafeGetImplementation(binding: IMethodBinding) = 18 | LAMBDA_METHOD_IMPL_FIELD.get(binding) as IMethodBinding 19 | 20 | /** 21 | * @author yawkat 22 | */ 23 | object Bindings { 24 | /** 25 | * A binding that is unique to the source file. Used for places where no other binding ID is available, 26 | * specifically initializer blocks and lambda expressions 27 | */ 28 | fun toStringKeyBinding(binding: IBinding): String = binding.key 29 | 30 | fun toString(typeBinding: ITypeBinding): String? { 31 | try { 32 | val decl = typeBinding.typeDeclaration.erasure ?: return null 33 | val qname = decl.qualifiedName 34 | // empty for local / anon types 35 | @Suppress("LiftReturnOrAssignment") 36 | if (qname.isEmpty()) { 37 | return decl.binaryName 38 | } else { 39 | return qname 40 | } 41 | } catch (e: UnsupportedOperationException) { 42 | return null 43 | } 44 | } 45 | 46 | fun toString(packageBinding: IPackageBinding): String? = 47 | if (packageBinding.isUnnamed) null 48 | else packageBinding.name 49 | 50 | fun toString(varBinding: IVariableBinding): String? { 51 | val decl = varBinding.variableDeclaration ?: return null 52 | if (decl.declaringClass == null) return null // array length for example 53 | return (toString(decl.declaringClass) ?: return null) + "#" + decl.name 54 | } 55 | 56 | fun toString(methodBinding: IMethodBinding): String? { 57 | var decl = methodBinding.methodDeclaration ?: return null 58 | if (LAMBDA_METHOD_CLASS.isInstance(decl)) { 59 | decl = unsafeGetImplementation(decl) 60 | } 61 | val builder = StringBuilder(toString(decl.declaringClass) ?: return null) 62 | if (!decl.isConstructor) { 63 | builder.append('#') 64 | builder.append(decl.name) 65 | } 66 | builder.append('(') 67 | for ((i, parameterType) in decl.parameterTypes.withIndex()) { 68 | if (i > 0) builder.append(',') 69 | builder.append(toString(parameterType) ?: return null) 70 | } 71 | builder.append(')') 72 | return builder.toString() 73 | } 74 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/JavadocRenderVisitor.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import at.yawk.javabrowser.RenderedJavadoc 5 | import at.yawk.javabrowser.generator.source.JavadocRenderer 6 | import at.yawk.javabrowser.generator.source.annotate 7 | import org.eclipse.jdt.core.dom.ASTNode 8 | import org.eclipse.jdt.core.dom.ASTVisitor 9 | import org.eclipse.jdt.core.dom.AbstractTypeDeclaration 10 | import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration 11 | import org.eclipse.jdt.core.dom.BodyDeclaration 12 | import org.eclipse.jdt.core.dom.EnumConstantDeclaration 13 | import org.eclipse.jdt.core.dom.FieldDeclaration 14 | import org.eclipse.jdt.core.dom.Javadoc 15 | import org.eclipse.jdt.core.dom.MethodDeclaration 16 | import org.eclipse.jdt.core.dom.VariableDeclaration 17 | import org.jsoup.nodes.Document 18 | 19 | class JavadocRenderVisitor( 20 | private val hashBinding: String.() -> BindingId, 21 | private val annotatedSourceFile: GeneratorSourceFile 22 | ) : ASTVisitor() { 23 | 24 | internal var lastVisited: ASTNode? = null 25 | 26 | private val outstanding = HashSet() 27 | private val done = HashSet() 28 | 29 | override fun visit(node: Javadoc): Boolean { 30 | outstanding.add(node) 31 | return false 32 | } 33 | 34 | override fun preVisit(node: ASTNode) { 35 | lastVisited = node 36 | 37 | if (node !is BodyDeclaration) return 38 | val javadoc = node.javadoc ?: return 39 | val declaringClass = if (node is AbstractTypeDeclaration) { 40 | node.resolveBinding() 41 | } else { 42 | (node.parent as? AbstractTypeDeclaration)?.resolveBinding() 43 | } 44 | val binding = when (node) { 45 | is AbstractTypeDeclaration -> node.resolveBinding() 46 | is AnnotationTypeMemberDeclaration -> node.resolveBinding() 47 | is EnumConstantDeclaration -> node.resolveVariable() 48 | is FieldDeclaration -> (node.fragments().singleOrNull() as? VariableDeclaration)?.resolveBinding() 49 | is MethodDeclaration -> node.resolveBinding() 50 | else -> null 51 | } 52 | 53 | done.add(javadoc) 54 | apply( 55 | javadoc, JavadocRenderer( 56 | hashBinding, 57 | declaringClassQualifiedName = declaringClass?.qualifiedName, 58 | subjectBinding = binding 59 | ) 60 | ) 61 | } 62 | 63 | fun finish() { 64 | outstanding.removeAll(done) 65 | outstanding.forEach { apply(it, JavadocRenderer(hashBinding)) } 66 | } 67 | 68 | private fun apply(javadoc: Javadoc, context: JavadocRenderer) { 69 | val renderedNodes = context.render(javadoc) 70 | val shell = Document.createShell("") 71 | shell.outputSettings().prettyPrint(false) 72 | shell.body().insertChildren(0, renderedNodes) 73 | val html = shell.body().html() 74 | annotatedSourceFile.annotate(javadoc, RenderedJavadoc(html)) 75 | } 76 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/metadata.ftl: -------------------------------------------------------------------------------- 1 | <#-- @ftlvariable name="artifactMetadata" type="at.yawk.javabrowser.ArtifactMetadata" --> 2 | -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/ArtifactUpdater.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.jdbi.v3.core.Jdbi 4 | import org.postgresql.PGConnection 5 | import org.slf4j.LoggerFactory 6 | import java.util.concurrent.Executor 7 | import java.util.concurrent.Executors 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * @author yawkat 13 | */ 14 | @Singleton 15 | class ArtifactUpdater @Inject constructor() { 16 | companion object { 17 | private val log = LoggerFactory.getLogger(ArtifactUpdater::class.java) 18 | } 19 | 20 | private val artifactListeners = ArrayList<(String) -> Unit>() 21 | private val invalidationListeners = ArrayList<() -> Unit>() 22 | 23 | private val pool: Executor 24 | 25 | init { 26 | var i = 0 27 | pool = Executors.newCachedThreadPool { Thread(it, "Artifact updater #${++i}") } 28 | } 29 | 30 | @Synchronized 31 | fun addArtifactUpdateListener(listener: (String) -> Unit) { 32 | artifactListeners.add(listener) 33 | } 34 | 35 | @Synchronized 36 | fun addInvalidationListener(runAtStart: Boolean = false, listener: () -> Unit) { 37 | invalidationListeners.add(listener) 38 | if (runAtStart) { 39 | pool.execute(listener) 40 | } 41 | } 42 | 43 | @Synchronized 44 | private fun onUpdate(artifactId: String) { 45 | for (listener in artifactListeners) { 46 | pool.execute { listener(artifactId) } 47 | } 48 | } 49 | 50 | @Synchronized 51 | private fun onInvalidate() { 52 | for (listener in invalidationListeners) { 53 | pool.execute(listener) 54 | } 55 | } 56 | 57 | fun listenForUpdates(dbi: Jdbi) { 58 | Thread({ 59 | while (true) { 60 | try { 61 | dbi.useHandle { 62 | it.createUpdate("listen artifact").execute() 63 | 64 | while (true) { 65 | val notifications = it.connection.unwrap(PGConnection::class.java).getNotifications(0) 66 | var invalidate = false 67 | for (notification in notifications) { 68 | if (notification.name != "artifact") { 69 | log.error("Received notification of name we did not listen to: {}", 70 | notification.name) 71 | continue 72 | } 73 | 74 | onUpdate(notification.parameter) 75 | invalidate = true 76 | } 77 | if (invalidate) { 78 | onInvalidate() 79 | } 80 | } 81 | } 82 | } catch (e: Exception) { 83 | log.error("Error during artifact update wait", e) 84 | } 85 | } 86 | }, "Artifact update thread").start() 87 | } 88 | } -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/ReferenceResourceIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.BindingRefType 4 | import at.yawk.javabrowser.Realm 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.google.common.base.Stopwatch 7 | import com.google.common.collect.ImmutableList 8 | import org.hamcrest.MatcherAssert 9 | import org.hamcrest.Matchers 10 | import org.jdbi.v3.core.Jdbi 11 | import org.testng.Assert 12 | import org.testng.annotations.BeforeClass 13 | import org.testng.annotations.Optional 14 | import org.testng.annotations.Parameters 15 | import org.testng.annotations.Test 16 | 17 | class ReferenceResourceIntegrationTest { 18 | private lateinit var dbi: Jdbi 19 | private lateinit var artifactIndex: ArtifactIndex 20 | private lateinit var referenceResource: ReferenceResource 21 | 22 | @Parameters("dataSource") 23 | @BeforeClass 24 | fun setUp(@Optional dataSource: String?) { 25 | dbi = loadIntegrationTestDbi(dataSource) 26 | artifactIndex = ArtifactIndex(ArtifactUpdater(), dbi) 27 | referenceResource = ReferenceResource(dbi, ObjectMapper(), artifactIndex) 28 | } 29 | 30 | @Test 31 | fun `short filter`() { 32 | dbi.inTransaction { conn -> 33 | val java8 = artifactIndex.allArtifactsByStringId["java/8"]!! 34 | val result = referenceResource.handleRequest( 35 | conn, 36 | Realm.SOURCE, 37 | "java.util.Observable", 38 | 100, 39 | java8 40 | ) 41 | val returnType = result.find { it?.type == BindingRefType.PARAMETER_TYPE }!! 42 | val items = ImmutableList.copyOf(returnType.items) 43 | Assert.assertEquals(items.count { it.artifactId == "java/8" }, 1) 44 | Assert.assertEquals(items.count { it.artifactId.startsWith("java/") }, 1) 45 | val first = items[0] 46 | Assert.assertEquals(first.artifactId, "java/8") 47 | } 48 | } 49 | 50 | @Test 51 | fun `large filter`() { 52 | dbi.inTransaction { conn -> 53 | val java8 = artifactIndex.allArtifactsByStringId["java/8"]!! 54 | val stopwatch = Stopwatch.createStarted() 55 | val result = referenceResource.handleRequest( 56 | conn, 57 | Realm.SOURCE, 58 | "java.lang.String", 59 | 100, 60 | java8 61 | ) 62 | val returnType = result.find { it?.type == BindingRefType.PARAMETER_TYPE }!! 63 | val items = ImmutableList.copyOf(returnType.items) 64 | stopwatch.stop() 65 | println(stopwatch.elapsed()) 66 | Assert.assertEquals(items.size, 100) 67 | MatcherAssert.assertThat( 68 | items, 69 | Matchers.not(Matchers.hasItem(matches { 70 | it.artifactId != "java/8" 71 | })) 72 | ) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/Escaper.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import org.eclipse.collections.api.map.primitive.CharObjectMap 4 | import org.eclipse.collections.impl.factory.primitive.CharObjectMaps 5 | import java.io.Writer 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | class Escaper private constructor(private val escapes: CharObjectMap) { 11 | companion object { 12 | val HTML = Escaper( 13 | '"' to """, 14 | '\'' to "'", 15 | '&' to "&", 16 | '<' to "<", 17 | '>' to ">" 18 | ) 19 | } 20 | 21 | private constructor(vararg escapes: Pair) : 22 | this(CharObjectMaps.mutable.from(escapes.asList(), { it.first }, { it.second })) 23 | 24 | private inline fun escape0( 25 | input: String, start: Int, end: Int, 26 | push: (String) -> Unit, 27 | copyRange: (Int, Int) -> Unit, 28 | copyRangeEnd: (Int) -> Unit 29 | ) { 30 | var fragmentStart = start 31 | for (i in start until end) { 32 | val c = input[i] 33 | val escape = escapes[c] 34 | if (escape != null) { 35 | if (i != fragmentStart) { 36 | copyRange(fragmentStart, i) 37 | } 38 | push(escape) 39 | fragmentStart = i + 1 40 | } 41 | } 42 | if (fragmentStart != end) { 43 | copyRangeEnd(fragmentStart) 44 | } 45 | } 46 | 47 | private inline fun escape0( 48 | input: String, start: Int, end: Int, 49 | push: (String) -> Unit, 50 | copyRange: (Int, Int) -> Unit 51 | ) = escape0(input, start, end, push, copyRange, { copyRange(it, end) }) 52 | 53 | fun escape(s: String, start: Int = 0, end: Int = s.length): String { 54 | var builder: StringBuilder? = null 55 | escape0( 56 | s, start, end, 57 | push = { 58 | if (builder == null) builder = StringBuilder(s.length + it.length + 16) 59 | 60 | builder!!.append(it) 61 | }, 62 | copyRange = { rangeStart, rangeEnd -> 63 | if (builder == null) builder = StringBuilder(s.length) 64 | builder!!.append(s, rangeStart, rangeEnd) 65 | }, 66 | copyRangeEnd = { rangeStart -> 67 | assert((builder == null) == (start == rangeStart)) 68 | if (builder != null) { 69 | builder!!.append(s, rangeStart, end) 70 | } // else no escape 71 | } 72 | ) 73 | return builder?.toString() ?: s.substring(start, end) 74 | } 75 | 76 | fun escape(writer: Writer, s: String, start: Int = 0, end: Int = s.length) { 77 | escape0( 78 | s, start, end, 79 | push = { writer.write(it) }, 80 | copyRange = { rangeStart, rangeEnd -> writer.write(s, rangeStart, rangeEnd - rangeStart) } 81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/Flag.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingDecl 4 | import org.objectweb.asm.Opcodes 5 | import java.util.EnumSet 6 | 7 | data class Flag( 8 | val opcode: Int, 9 | val id: String, 10 | val modifier: String?, 11 | val targets: Set 12 | ) { 13 | private constructor(opcode: Int, id: String, modifier: String?, vararg targets: Target) : 14 | this(opcode, id, modifier, EnumSet.copyOf(targets.asList())) 15 | 16 | companion object { 17 | val FLAGS = listOf( 18 | Flag(Opcodes.ACC_PUBLIC, "ACC_PUBLIC", "public", Target.CLASS, Target.FIELD, Target.METHOD), 19 | Flag(Opcodes.ACC_PRIVATE, "ACC_PRIVATE", "private", Target.CLASS, Target.FIELD, Target.METHOD), 20 | Flag(Opcodes.ACC_PROTECTED, "ACC_PROTECTED", "protected", Target.CLASS, Target.FIELD, Target.METHOD), 21 | Flag(Opcodes.ACC_STATIC, "ACC_STATIC", "static", Target.FIELD, Target.METHOD), 22 | Flag(Opcodes.ACC_FINAL, "ACC_FINAL", "final", Target.CLASS, Target.FIELD, Target.METHOD, Target.PARAMETER), 23 | Flag(Opcodes.ACC_SUPER, "ACC_SUPER", null, Target.CLASS), 24 | Flag(Opcodes.ACC_SYNCHRONIZED, "ACC_SYNCHRONIZED", "synchronized", Target.METHOD), 25 | Flag(Opcodes.ACC_OPEN, "ACC_OPEN", "synchronized", Target.MODULE), 26 | Flag(Opcodes.ACC_TRANSITIVE, "ACC_TRANSITIVE", "transitive", Target.REQUIRES), 27 | Flag(Opcodes.ACC_VOLATILE, "ACC_VOLATILE", "volatile", Target.FIELD), 28 | Flag(Opcodes.ACC_BRIDGE, "ACC_BRIDGE", null, Target.METHOD), 29 | Flag(Opcodes.ACC_STATIC_PHASE, "ACC_STATIC_PHASE", "static", Target.REQUIRES), 30 | Flag(Opcodes.ACC_VARARGS, "ACC_VARARGS", null, Target.METHOD), 31 | Flag(Opcodes.ACC_TRANSIENT, "ACC_TRANSIENT", "transient", Target.FIELD), 32 | Flag(Opcodes.ACC_NATIVE, "ACC_NATIVE", "native", Target.METHOD), 33 | Flag(Opcodes.ACC_INTERFACE, "ACC_INTERFACE", null, Target.CLASS), 34 | Flag(Opcodes.ACC_ABSTRACT, "ACC_ABSTRACT", "abstract", Target.CLASS, Target.METHOD), 35 | Flag(Opcodes.ACC_STRICT, "ACC_STRICT", "strict", Target.METHOD), 36 | Flag(Opcodes.ACC_SYNTHETIC, "ACC_SYNTHETIC", null, EnumSet.allOf(Target::class.java)), 37 | Flag(Opcodes.ACC_ANNOTATION, "ACC_ANNOTATION", null, Target.CLASS), 38 | Flag(Opcodes.ACC_ENUM, "ACC_ANNOTATION", null, Target.CLASS, Target.FIELD), 39 | Flag(Opcodes.ACC_MANDATED, "ACC_MANDATED", null, EnumSet.allOf(Target::class.java)), 40 | Flag(Opcodes.ACC_MODULE, "ACC_MODULE", null, Target.CLASS) 41 | ) 42 | } 43 | 44 | enum class Target { 45 | CLASS, FIELD, METHOD, PARAMETER, MODULE, REQUIRES, EXPORTS, OPENS 46 | } 47 | } 48 | 49 | fun asmAccessToSourceAnnotation(asm: Int): Int { 50 | var transformed = asm and 0xffff 51 | if ((asm and Opcodes.ACC_DEPRECATED) != 0) transformed = transformed or BindingDecl.MODIFIER_DEPRECATED 52 | return transformed 53 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/Ftl.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.view.View 4 | import com.google.common.annotations.VisibleForTesting 5 | import freemarker.core.HTMLOutputFormat 6 | import freemarker.ext.beans.BeansWrapperBuilder 7 | import freemarker.template.Configuration 8 | import freemarker.template.SimpleScalar 9 | import freemarker.template.TemplateExceptionHandler 10 | import io.undertow.server.HttpServerExchange 11 | import io.undertow.util.Headers 12 | import java.io.OutputStream 13 | import java.io.OutputStreamWriter 14 | import java.lang.reflect.Modifier 15 | import javax.inject.Inject 16 | import javax.inject.Singleton 17 | 18 | private val THEMES = setOf("default", "darcula") 19 | 20 | @Singleton 21 | class Ftl @Inject constructor(imageCache: ImageCache) { 22 | private val configuration = Configuration(Configuration.VERSION_2_3_28) 23 | 24 | init { 25 | configuration.setClassLoaderForTemplateLoading(Ftl::class.java.classLoader, "/at/yawk/javabrowser/server/view") 26 | configuration.defaultEncoding = "UTF-8" 27 | configuration.templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER 28 | configuration.logTemplateExceptions = false 29 | configuration.wrapUncheckedExceptions = true 30 | configuration.outputFormat = HTMLOutputFormat.INSTANCE 31 | configuration.whitespaceStripping = true 32 | configuration.urlEscapingCharset = "UTF-8" 33 | 34 | val statics = BeansWrapperBuilder(Configuration.VERSION_2_3_28).build().staticModels 35 | configuration.setSharedVariable("Modifier", statics[Modifier::class.qualifiedName]) 36 | configuration.setSharedVariable("ConservativeLoopBlock", ConservativeLoopBlock()) 37 | configuration.setSharedVariable("Path", PathDirective()) 38 | 39 | configuration.setSharedVariable("imageCache", imageCache.directive) 40 | } 41 | 42 | fun render(exchange: HttpServerExchange, view: View) { 43 | val themeCookie = exchange.requestCookies["theme"]?.value 44 | val javadocRenderEnabledCookie = exchange.requestCookies["javadoc-render-enabled"]?.value 45 | exchange.responseHeaders.put(Headers.CONTENT_TYPE, "text/html") 46 | render(view, exchange.outputStream, 47 | theme = if (themeCookie in THEMES) themeCookie else "default", 48 | javadocRenderEnabled = javadocRenderEnabledCookie?.toBoolean() ?: false) 49 | } 50 | 51 | @VisibleForTesting 52 | internal fun render( 53 | view: View, 54 | outputStream: OutputStream, 55 | theme: String?, 56 | javadocRenderEnabled: Boolean 57 | ) { 58 | val template = configuration.getTemplate(view.templateFile) 59 | OutputStreamWriter(outputStream).use { 60 | val processingEnvironment = template.createProcessingEnvironment(view, it) 61 | processingEnvironment.setGlobalVariable("theme", SimpleScalar(theme)) 62 | processingEnvironment.setGlobalVariable("javadocRenderEnabled", 63 | configuration.objectWrapper.wrap(javadocRenderEnabled)) 64 | processingEnvironment.process() 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/ResumingUrlInputStream.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | import java.net.URL 6 | import java.util.regex.Pattern 7 | 8 | private val CONTENT_RANGE_PATTERN = Pattern.compile("bytes (?\\d+)-(?\\d+)/(?\\d+)") 9 | 10 | /** 11 | * Wrapper around [URL#openConnection] that reopens closed connections with appropriate `Range` headers to continue a 12 | * download in progress. 13 | */ 14 | class ResumingUrlInputStream(private val url: URL) : InputStream() { 15 | private var delegate: InputStream? = null 16 | private var position: Long = 0 17 | private var fullSize: Long = -1 18 | 19 | private val eof: Boolean 20 | get() = fullSize in 0..position 21 | 22 | private fun reopen() { 23 | if (eof) { 24 | throw IllegalStateException("Already hit EOF") 25 | } 26 | val connection = url.openConnection() 27 | if (position == 0L) { 28 | delegate = connection.getInputStream() 29 | fullSize = connection.getHeaderFieldLong("content-length", -1) 30 | } else { 31 | connection.setRequestProperty("Range", "bytes=$position-") 32 | delegate = connection.getInputStream() 33 | } 34 | val contentRange = connection.getHeaderField("Content-Range") 35 | if (contentRange != null) { 36 | val matcher = CONTENT_RANGE_PATTERN.matcher(contentRange) 37 | if (!matcher.matches()) { 38 | throw IOException("Unsupported Content-Range in response: $contentRange") 39 | } 40 | val start = matcher.group("start").toLong() 41 | val size = matcher.group("size").toLong() 42 | if (start < this.position) { 43 | delegate!!.skip(this.position - start) 44 | } else if (start > this.position) { 45 | throw IOException("response Content-Range start after requested position") 46 | } 47 | this.fullSize = size 48 | } 49 | if (fullSize < 0L) { 50 | throw IOException("Unknown file size") 51 | } 52 | } 53 | 54 | override fun read(): Int { 55 | while (true) { 56 | if (delegate == null) { 57 | if (eof) { 58 | return -1 59 | } 60 | reopen() 61 | } 62 | val value = delegate!!.read() 63 | if (value == -1) { 64 | delegate = null 65 | } else { 66 | position++ 67 | return value 68 | } 69 | } 70 | } 71 | 72 | override fun read(b: ByteArray, off: Int, len: Int): Int { 73 | while (true) { 74 | if (delegate == null) { 75 | if (eof) { 76 | return -1 77 | } 78 | reopen() 79 | } 80 | val n = delegate!!.read(b, off, len) 81 | if (n == -1) { 82 | delegate = null 83 | } else { 84 | position += n 85 | return n 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/artifact/ArtifactNode.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server.artifact 2 | 3 | import at.yawk.javabrowser.server.VersionComparator 4 | import org.eclipse.collections.api.map.primitive.LongObjectMap 5 | import org.eclipse.collections.impl.factory.primitive.LongObjectMaps 6 | import java.util.NavigableMap 7 | import java.util.TreeMap 8 | 9 | /** 10 | * @author yawkat 11 | */ 12 | class ArtifactNode private constructor( 13 | val idInParent: String?, 14 | val parent: ArtifactNode?, 15 | childrenNames: List 16 | ) { 17 | companion object { 18 | fun build(artifacts: List): ArtifactNode { 19 | return ArtifactNode(null, null, artifacts) 20 | } 21 | } 22 | 23 | val stringIdList: List = 24 | if (idInParent == null) emptyList() 25 | else parent!!.stringIdList + idInParent 26 | val stringId: String = stringIdList.joinToString("/") 27 | 28 | val dbId = childrenNames.singleOrNull { it.components.isEmpty() }?.dbId 29 | 30 | val children = childrenNames 31 | .filter { it.components.isNotEmpty() } 32 | .groupBy { it.components.first() } 33 | .mapValues { (k, v) -> ArtifactNode(k, this, v.map { Prototype(it.dbId, it.components.drop(1)) }) } 34 | 35 | /** 36 | * If this node has only one child, return that node. 37 | */ 38 | private fun flatten(): ArtifactNode = children.values.singleOrNull()?.flatten() ?: this 39 | 40 | /** 41 | * A "flattened" view of this node's children. If this node is A and: 42 | * 43 | * ``` 44 | * A 45 | * / \ 46 | * B C 47 | * / \ \ 48 | * D E F 49 | * ``` 50 | * 51 | * then A's flattenedChildren are `[B, F]`. 52 | * 53 | * This is used to display a compact representation of this node's children while still not having the user click 54 | * through multiple levels when there is only one choice anyway. 55 | */ 56 | val flattenedChildren: List = 57 | children.values 58 | .map { it.flatten() } 59 | .sortedWith(compareBy(VersionComparator) { it.stringId }) 60 | 61 | val allNodesByStringId: NavigableMap 62 | val allNodesByDbId: LongObjectMap 63 | 64 | init { 65 | allNodesByStringId = TreeMap(VersionComparator) 66 | allNodesByStringId[this.stringId] = this 67 | for (child in children.values) { 68 | allNodesByStringId.putAll(child.allNodesByStringId) 69 | } 70 | 71 | allNodesByDbId = LongObjectMaps.mutable.empty().also { 72 | if (dbId != null) it.put(dbId, this) 73 | for (child in children.values) { 74 | it.putAll(child.allNodesByDbId) 75 | } 76 | } 77 | } 78 | 79 | class Prototype constructor( 80 | val dbId: Long, 81 | val components: List 82 | ) { 83 | constructor(dbId: Long, stringId: String) : this(dbId = dbId, components = stringId.split('/')) 84 | } 85 | 86 | override fun toString(): String { 87 | return "ArtifactNode($dbId: $stringId)" 88 | } 89 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/db/ActorAsyncTransactionProviderTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.db 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.test.TestScope 6 | import kotlinx.coroutines.test.advanceUntilIdle 7 | import org.testng.Assert 8 | import org.testng.annotations.Test 9 | 10 | private const val PRINT = false 11 | 12 | @ExperimentalCoroutinesApi 13 | class ActorAsyncTransactionProviderTest { 14 | private fun TestScope.step(name: String) { 15 | if (PRINT) println("Step $name") 16 | advanceUntilIdle() 17 | if (PRINT) println("/Step $name") 18 | } 19 | 20 | @Test 21 | fun test() { 22 | val workerScope = TestScope() 23 | var actualTx: TestTx? = null 24 | val provider = ActorAsyncTransactionProvider( 25 | workerScope = workerScope, 26 | actorQueueCapacity = 2, 27 | delegate = object : TransactionProvider { 28 | override suspend fun claimArtifactId() = throw AssertionError() 29 | 30 | override suspend fun withArtifactTransaction(artifactId: String, task: suspend (Transaction) -> Unit) { 31 | actualTx = TestTx() 32 | task(actualTx!!) 33 | actualTx!!.closed = true 34 | } 35 | } 36 | ) 37 | Assert.assertNull(actualTx) 38 | val submitterScope = TestScope() 39 | var submittedTasks = 0 40 | val run = submitterScope.async { 41 | provider.withArtifactTransaction("foo") { 42 | for (i in 0 until 6) { 43 | if (PRINT) println("submit $submittedTasks") 44 | submittedTasks++ 45 | it.insertAlias(0, "bar") 46 | } 47 | } 48 | } 49 | 50 | Assert.assertEquals(submittedTasks, 0) 51 | 52 | submitterScope.step("submitter") 53 | // should now be suspended in the submit. 54 | Assert.assertNotNull(actualTx) 55 | Assert.assertEquals(submittedTasks, 3) 56 | 57 | workerScope.step("worker") 58 | // done the three calls. 59 | Assert.assertEquals(actualTx!!.callCount, 3) 60 | Assert.assertEquals(submittedTasks, 3) 61 | 62 | submitterScope.step("submitter") 63 | // should now be suspended in the final submit. 64 | Assert.assertEquals(submittedTasks, 6) 65 | Assert.assertEquals(actualTx!!.callCount, 3) 66 | 67 | workerScope.step("worker") 68 | // all tasks executed, but the submitter should still be suspending, waiting for the worker to complete 69 | Assert.assertFalse(run.isCompleted) 70 | Assert.assertEquals(submittedTasks, 6) 71 | Assert.assertEquals(actualTx!!.callCount, 6) 72 | 73 | submitterScope.step("submitter") 74 | // done! 75 | Assert.assertTrue(actualTx!!.closed) 76 | Assert.assertTrue(run.isCompleted) 77 | } 78 | 79 | private class TestTx : TestTransaction() { 80 | var closed = false 81 | var callCount = 0 82 | 83 | override suspend fun onAnyTask() { 84 | if (PRINT) println("perform") 85 | require(!closed) 86 | callCount++ 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/work/PrepareGraalWorkerTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.work 2 | 3 | import at.yawk.javabrowser.ArtifactMetadata 4 | import at.yawk.javabrowser.generator.ArtifactConfig 5 | import at.yawk.javabrowser.generator.MavenDependencyResolver 6 | import at.yawk.javabrowser.generator.db.TestTransaction 7 | import at.yawk.javabrowser.generator.db.Transaction 8 | import at.yawk.javabrowser.generator.db.TransactionProvider 9 | import kotlinx.coroutines.GlobalScope 10 | import kotlinx.coroutines.runBlocking 11 | import org.testng.Assert 12 | import org.testng.annotations.Test 13 | import java.net.URL 14 | 15 | class PrepareGraalWorkerTest { 16 | @Test 17 | fun `parse suite`() { 18 | val suite = PrepareGraalWorker.parseSuite(""" 19 | suite = { 20 | "name": "test", 21 | "projects": { 22 | "at.yawk": { 23 | "subDir": "src", 24 | "sourceDirs": ["src"], 25 | "dependencies": ["sdk:GRAAL_SDK"] 26 | } 27 | } 28 | } 29 | """) 30 | Assert.assertEquals( 31 | suite, 32 | PrepareGraalWorker.Suite( 33 | name = "test", 34 | projects = mapOf( 35 | "at.yawk" to PrepareGraalWorker.Suite.Project( 36 | "src", 37 | listOf("src"), 38 | listOf("sdk:GRAAL_SDK") 39 | ) 40 | ) 41 | ) 42 | ) 43 | } 44 | 45 | @Test(groups = ["longRunningDownload"]) 46 | fun `download test`() { 47 | val config = ArtifactConfig.Graal( 48 | repos = listOf( 49 | URL("https://github.com/graalvm/mx/archive/91fbc57445fc24bf2d20b57a5a0a7caf82a8b3aa.zip"), 50 | URL("https://github.com/oracle/graal/archive/vm-21.0.0.2.zip"), 51 | URL("https://github.com/oracle/graaljs/archive/vm-21.0.0.2.zip"), 52 | URL("https://github.com/oracle/graalpython/archive/vm-21.0.0.2.zip"), 53 | URL("https://github.com/oracle/truffleruby/archive/vm-21.0.0.2.zip"), 54 | ), 55 | version = "21.0.0.2", 56 | jdk = URL("https://github.com/graalvm/labs-openjdk-11/releases/download/jvmci-19.3-b25/labsjdk-ce-11.0.11+8-jvmci-19.3-b25-linux-amd64.tar.gz"), 57 | metadata = ArtifactMetadata() 58 | ) 59 | runBlocking { 60 | CompileWorker( 61 | transactionProvider = object : TransactionProvider { 62 | override suspend fun claimArtifactId() = 0L 63 | 64 | override suspend fun withArtifactTransaction( 65 | artifactId: String, 66 | task: suspend (Transaction) -> Unit 67 | ) { 68 | task(TestTransaction()) 69 | } 70 | }, 71 | acceptScope = GlobalScope 72 | ).forArtifact("graal/21.0.0.2") { compileWorker -> 73 | val prepWorker = PrepareGraalWorker(TempDirProviderTest, MavenDependencyResolver()) 74 | prepWorker.prepareArtifact( 75 | prepWorker.getArtifactId(config), 76 | config, 77 | compileWorker 78 | ) 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/at/yawk/javabrowser/server/diff.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.server.view.DeclarationNode 4 | import com.google.common.base.Equivalence 5 | import org.eclipse.jgit.diff.DiffAlgorithm 6 | import org.eclipse.jgit.diff.Edit 7 | import org.eclipse.jgit.diff.Sequence 8 | import org.eclipse.jgit.diff.SequenceComparator 9 | 10 | /** 11 | * Diff two lists. 12 | */ 13 | fun buildDiffEdits( 14 | newItems: List, 15 | oldItems: List, 16 | equivalence: Equivalence 17 | ) = DiffAlgorithm.getAlgorithm(DiffAlgorithm.SupportedAlgorithm.HISTOGRAM) 18 | .diff>(ComparatorImpl(equivalence), SequenceImpl(oldItems), SequenceImpl(newItems))!! 19 | 20 | /** 21 | * Iterate over the changed and unchanged regions of a diff. 22 | */ 23 | inline fun iterateEdits( 24 | diff: List, 25 | newLength: Int, 26 | oldLength: Int, 27 | visitUnchanged: (IntRange, IntRange) -> Unit, 28 | visitChanged: (IntRange, IntRange) -> Unit 29 | ) { 30 | var newI = 0 31 | var oldI = 0 32 | 33 | for (edit in diff) { 34 | if (newI < edit.beginB) { 35 | require(oldI < edit.beginA) 36 | visitUnchanged( 37 | newI until edit.beginB, 38 | oldI until edit.beginA 39 | ) 40 | } 41 | 42 | visitChanged( 43 | edit.beginB until edit.endB, 44 | edit.beginA until edit.endA 45 | ) 46 | newI = edit.endB 47 | oldI = edit.endA 48 | } 49 | if (newI < newLength) { 50 | visitUnchanged( 51 | newI until newLength, 52 | oldI until oldLength 53 | ) 54 | } 55 | } 56 | 57 | /** 58 | * Transform a diff as returned by [buildDiffEdits] to a list of additions/deletions or unchanged items 59 | */ 60 | inline fun mapEdits( 61 | newItems: List, 62 | oldItems: List, 63 | diff: List, 64 | mapItem: (T, DeclarationNode.DiffResult) -> R 65 | ): List { 66 | val result = ArrayList() 67 | iterateEdits( 68 | diff, 69 | newLength = newItems.size, 70 | oldLength = oldItems.size, 71 | visitUnchanged = { newR, oldR -> 72 | require(newR.first - newR.last == oldR.first - oldR.last) 73 | result.addAll( 74 | newItems.subList(newR.first, newR.last + 1).map { mapItem(it, DeclarationNode.DiffResult.UNCHANGED) }) 75 | }, 76 | visitChanged = { newR, oldR -> 77 | result.addAll( 78 | oldItems.subList(oldR.first, oldR.last + 1).map { mapItem(it, DeclarationNode.DiffResult.DELETION) }) 79 | result.addAll( 80 | newItems.subList(newR.first, newR.last + 1).map { mapItem(it, DeclarationNode.DiffResult.INSERTION) }) 81 | } 82 | ) 83 | return result 84 | } 85 | 86 | private class SequenceImpl(val items: List) : Sequence() { 87 | override fun size() = items.size 88 | } 89 | 90 | private class ComparatorImpl(private val equivalence: Equivalence) : SequenceComparator>() { 91 | override fun hash(seq: SequenceImpl, ptr: Int) = equivalence.hash(seq.items[ptr]) 92 | 93 | override fun equals(a: SequenceImpl, ai: Int, b: SequenceImpl, bi: Int) = 94 | equivalence.equivalent(a.items[ai], b.items[bi]) 95 | } 96 | -------------------------------------------------------------------------------- /server/src/test/kotlin/at/yawk/javabrowser/server/FullTextSearchResourceIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.server 2 | 3 | import at.yawk.javabrowser.Realm 4 | import at.yawk.javabrowser.server.view.FullTextSearchResultView 5 | import com.google.common.collect.ImmutableList 6 | import org.hamcrest.MatcherAssert 7 | import org.hamcrest.Matchers 8 | import org.jdbi.v3.core.Jdbi 9 | import org.testng.annotations.BeforeClass 10 | import org.testng.annotations.Optional 11 | import org.testng.annotations.Parameters 12 | import org.testng.annotations.Test 13 | 14 | class FullTextSearchResourceIntegrationTest { 15 | private lateinit var dbi: Jdbi 16 | private lateinit var artifactIndex: ArtifactIndex 17 | private lateinit var fts: FullTextSearchResource 18 | 19 | @Parameters("dataSource") 20 | @BeforeClass 21 | fun setUp(@Optional dataSource: String?) { 22 | dbi = loadIntegrationTestDbi(dataSource) 23 | artifactIndex = ArtifactIndex(ArtifactUpdater(), dbi) 24 | fts = FullTextSearchResource(dbi, Ftl(ImageCache()), BindingResolver(ArtifactUpdater(), dbi, artifactIndex), artifactIndex) 25 | } 26 | 27 | @Test 28 | fun string() { 29 | dbi.withHandle { conn -> 30 | val view = fts.handleRequest( 31 | query = "count is negative", 32 | realm = Realm.SOURCE, 33 | searchArtifact = null, 34 | useSymbolsParameter = null, 35 | conn = conn 36 | ) 37 | val results = ImmutableList.copyOf(view.results) 38 | MatcherAssert.assertThat( 39 | results, 40 | Matchers.hasItem(matches { 41 | it.artifactId == "java/8" && 42 | it.path == "java/lang/String.java" 43 | }) 44 | ) 45 | conn.rollback() 46 | } 47 | } 48 | 49 | @Test 50 | fun `string try render`() { 51 | dbi.withHandle { conn -> 52 | val view = fts.handleRequest( 53 | query = "count is negative", 54 | realm = Realm.SOURCE, 55 | searchArtifact = null, 56 | useSymbolsParameter = null, 57 | conn = conn 58 | ) 59 | tryRender(view) 60 | conn.rollback() 61 | } 62 | } 63 | 64 | @Test 65 | fun symbols() { 66 | dbi.withHandle { conn -> 67 | val view = fts.handleRequest( 68 | query = "count < 0", 69 | realm = Realm.SOURCE, 70 | searchArtifact = artifactIndex.allArtifactsByStringId["java/8"], 71 | useSymbolsParameter = null, 72 | conn = conn 73 | ) 74 | val results = ImmutableList.copyOf(view.results) 75 | MatcherAssert.assertThat( 76 | results, 77 | Matchers.hasItem(matches { 78 | it.artifactId == "java/8" && 79 | it.path == "java/lang/String.java" 80 | }) 81 | ) 82 | conn.rollback() 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/page.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#macro page realm title newPath oldPath="" hasSearch=false additionalTitle="" additionalMenu="" narrow=false tooltip=false> 3 | 4 | 5 | <#-- @ftlvariable name="newPath" type="at.yawk.javabrowser.server.ParsedPath" --> 6 | <#-- @ftlvariable name="oldPath" type="at.yawk.javabrowser.server.ParsedPath" --> 7 | <#-- @ftlvariable name="title" type="java.lang.String" --> 8 | 9 | 11 | 12 | ${title} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 41 |
class="narrow"> 42 |
43 | <#nested/> 44 |
45 |
46 | 62 | <#if hasSearch> 63 | <#include "search-dialog.ftl"> 64 | 65 |
66 | <#if tooltip> 67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /shared/src/test/kotlin/at/yawk/javabrowser/CborMapperTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.dataformat.cbor.CBORFactory 5 | import org.testng.Assert 6 | import org.testng.annotations.Test 7 | 8 | class CborMapperTest { 9 | private val compressedMapper = ObjectMapper(CompressedFactory()).findAndRegisterModules() 10 | private val normalMapper = ObjectMapper(CBORFactory()).findAndRegisterModules() 11 | 12 | @Test 13 | fun testFieldName() { 14 | testFieldName("id", 1, A("irrelevant")) 15 | testFieldName("annotation", 1, B("irrelevant")) 16 | testFieldName("javadoc", 2, C("irrelevant")) 17 | } 18 | 19 | @Test 20 | fun testFieldValue() { 21 | for (s in CommonStrings.COMMON_STRINGS_1) { 22 | testFieldValue(s, 1) 23 | } 24 | for (s in CommonStrings.COMMON_STRINGS_2) { 25 | testFieldValue(s, 2) 26 | } 27 | } 28 | 29 | data class A(val id: String) 30 | data class B(val annotation: String) 31 | data class C(val javadoc: String) 32 | data class NotCompressed(val `not compressed`: String) 33 | 34 | private inline fun testFieldName(s: String, expectedLength: Int, obj: C) { 35 | val normalBytes = normalMapper.writeValueAsBytes(obj) 36 | val compressedBytes = compressedMapper.writeValueAsBytes(obj) 37 | Assert.assertEquals( 38 | compressedBytes.size, 39 | // normal: 1 byte header + string data 40 | // compressed: expectedLength 41 | normalBytes.size - (s.length + 1) + expectedLength 42 | ) 43 | Assert.assertEquals( 44 | normalMapper.readValue(normalBytes, C::class.java), 45 | obj, 46 | "sanity check" 47 | ) 48 | Assert.assertEquals( 49 | compressedMapper.readValue(normalBytes, C::class.java), 50 | obj, 51 | "should still be able to read normal ser" 52 | ) 53 | Assert.assertEquals( 54 | compressedMapper.readValue(compressedBytes, C::class.java), 55 | obj, 56 | "should be able to read compressed ser" 57 | ) 58 | } 59 | 60 | private fun testFieldValue(s: String, expectedLength: Int) { 61 | val obj = NotCompressed(s) 62 | val normalBytes = normalMapper.writeValueAsBytes(obj) 63 | val compressedBytes = compressedMapper.writeValueAsBytes(obj) 64 | Assert.assertEquals( 65 | compressedBytes.size, 66 | // normal: 1 byte header + string data 67 | // compressed: 1 byte tag + expectedLength 68 | normalBytes.size - (s.length + 1) + expectedLength + 1, 69 | "compressed size for '$s'" 70 | ) 71 | Assert.assertEquals( 72 | normalMapper.readValue(normalBytes, NotCompressed::class.java), 73 | obj, 74 | "sanity check" 75 | ) 76 | Assert.assertEquals( 77 | compressedMapper.readValue(normalBytes, NotCompressed::class.java), 78 | obj, 79 | "should still be able to read normal ser" 80 | ) 81 | Assert.assertEquals( 82 | compressedMapper.readValue(compressedBytes, NotCompressed::class.java), 83 | obj, 84 | "should be able to read compressed ser" 85 | ) 86 | } 87 | } -------------------------------------------------------------------------------- /shared/src/test/kotlin/at/yawk/javabrowser/IntRangeSetTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser 2 | 3 | import at.yawk.javabrowser.IntRangeSet 4 | import org.testng.Assert 5 | import org.testng.annotations.Test 6 | 7 | /** 8 | * @author yawkat 9 | */ 10 | class IntRangeSetTest { 11 | @Test 12 | fun `test toString`() { 13 | val set = IntRangeSet() 14 | set.add(5, 6) 15 | set.add(7, 8) 16 | Assert.assertEquals( 17 | set.toString(), 18 | "{[5,6), [7,8)}" 19 | ) 20 | } 21 | 22 | @Test 23 | fun `coalesce down`() { 24 | val set = IntRangeSet() 25 | set.add(5, 10) 26 | set.add(7, 15) 27 | Assert.assertEquals( 28 | set.toString(), 29 | "{[5,15)}" 30 | ) 31 | } 32 | 33 | @Test 34 | fun `coalesce up`() { 35 | val set = IntRangeSet() 36 | set.add(7, 15) 37 | set.add(5, 10) 38 | Assert.assertEquals( 39 | set.toString(), 40 | "{[5,15)}" 41 | ) 42 | } 43 | 44 | @Test 45 | fun `coalesce bidi`() { 46 | val set = IntRangeSet() 47 | set.add(5, 10) 48 | set.add(15, 20) 49 | set.add(7, 15) 50 | Assert.assertEquals( 51 | set.toString(), 52 | "{[5,20)}" 53 | ) 54 | } 55 | 56 | @Test 57 | fun `expand`() { 58 | val set = IntRangeSet() 59 | set.add(5, 8) 60 | set.add(0, 9) 61 | Assert.assertEquals( 62 | set.toString(), 63 | "{[0,9)}" 64 | ) 65 | } 66 | 67 | @Test 68 | fun `coalesce smaller`() { 69 | val set = IntRangeSet() 70 | set.add(5, 10) 71 | set.add(7, 8) 72 | Assert.assertEquals( 73 | set.toString(), 74 | "{[5,10)}" 75 | ) 76 | } 77 | 78 | @Test 79 | fun contains() { 80 | val set = IntRangeSet() 81 | set.add(5, 10) 82 | Assert.assertTrue(set.contains(7)) 83 | Assert.assertTrue(set.contains(5)) 84 | Assert.assertTrue(set.contains(9)) 85 | Assert.assertFalse(set.contains(10)) 86 | Assert.assertFalse(set.contains(4)) 87 | Assert.assertFalse(set.contains(0)) 88 | } 89 | 90 | @Test 91 | fun encloses() { 92 | val set = IntRangeSet() 93 | set.add(5, 10) 94 | Assert.assertTrue(set.encloses(5, 10)) 95 | Assert.assertTrue(set.encloses(6, 9)) 96 | Assert.assertTrue(set.encloses(1, 1)) 97 | Assert.assertFalse(set.encloses(2, 3)) 98 | Assert.assertFalse(set.encloses(4, 6)) 99 | Assert.assertFalse(set.encloses(9, 11)) 100 | } 101 | 102 | @Test 103 | fun intersects() { 104 | val set = IntRangeSet() 105 | set.add(5, 10) 106 | Assert.assertTrue(set.intersects(5, 10)) 107 | Assert.assertTrue(set.intersects(6, 9)) 108 | Assert.assertTrue(set.intersects(4, 6)) 109 | Assert.assertFalse(set.intersects(4, 5)) 110 | Assert.assertTrue(set.intersects(9, 11)) 111 | Assert.assertTrue(set.intersects(9, 10)) 112 | Assert.assertFalse(set.intersects(1, 5)) 113 | Assert.assertFalse(set.intersects(10, 11)) 114 | Assert.assertFalse(set.intersects(11, 12)) 115 | Assert.assertFalse(set.intersects(1, 1)) 116 | Assert.assertTrue(set.intersects(4, 11)) 117 | Assert.assertTrue(set.intersects(2, 15)) 118 | } 119 | } -------------------------------------------------------------------------------- /generator/src/main/kotlin/at/yawk/javabrowser/generator/bytecode/bindings.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingRefType 4 | import at.yawk.javabrowser.Style 5 | import org.objectweb.asm.Type 6 | 7 | 8 | /** 9 | * @param duplicate see [at.yawk.javabrowser.BindingRef.duplicate] 10 | */ 11 | fun BytecodePrinter.appendDescriptor(type: Type, refType: BindingRefType, duplicate: Boolean = false): BytecodePrinter { 12 | when (type.sort) { 13 | Type.OBJECT -> { 14 | annotate(createBindingRef(refType, BytecodeBindings.toStringClass(type), duplicate)) { 15 | append(type.descriptor) 16 | } 17 | } 18 | Type.ARRAY -> { 19 | append("[".repeat(type.dimensions)) 20 | appendDescriptor(type.elementType, refType) 21 | } 22 | Type.METHOD -> appendMethodDescriptor(type, refType, refType, duplicate) 23 | else -> append(type.descriptor.single()) 24 | } 25 | return this 26 | } 27 | 28 | fun BytecodePrinter.appendMethodDescriptor(type: Type, 29 | paramRefType: BindingRefType, 30 | returnRefType: BindingRefType, 31 | duplicate: Boolean = false): BytecodePrinter { 32 | require(type.sort == Type.METHOD) 33 | append('(') 34 | for (argumentType in type.argumentTypes) { 35 | appendDescriptor(argumentType, paramRefType, duplicate) 36 | } 37 | append(')') 38 | appendDescriptor(type.returnType, returnRefType, duplicate) 39 | return this 40 | } 41 | 42 | /** 43 | * Append a field or method reference. 44 | * 45 | * @param type The type of this member. [Type.METHOD] for methods, any other for fields. 46 | * @param duplicate see [at.yawk.javabrowser.BindingRef.duplicate] 47 | */ 48 | fun BytecodePrinter.appendMember(owner: Type, name: String, type: Type, refType: BindingRefType, duplicate: Boolean = false): BytecodePrinter { 49 | val ref = 50 | if (type.sort == Type.METHOD) BytecodeBindings.toStringMethod(owner, name, type) 51 | else BytecodeBindings.toStringField(owner, name, type) 52 | appendJavaName(owner, BindingRefType.MEMBER_REFERENCE_QUALIFIER).append('.') 53 | annotate(createBindingRef(refType, ref, duplicate)) { 54 | append(name) 55 | } 56 | append(':').appendDescriptor(type, BindingRefType.MEMBER_REFERENCE_QUALIFIER) 57 | return this 58 | } 59 | 60 | /** 61 | * @param duplicate see [at.yawk.javabrowser.BindingRef.duplicate] 62 | */ 63 | fun BytecodePrinter.appendJavaName(type: Type, refType: BindingRefType, duplicate: Boolean = false): BytecodePrinter { 64 | require(type.sort != Type.METHOD) 65 | when (type.sort) { 66 | Type.ARRAY -> { 67 | appendJavaName(type.elementType, refType) 68 | append("[]".repeat(type.dimensions)) 69 | } 70 | Type.OBJECT -> { 71 | annotate(createBindingRef(refType, BytecodeBindings.toStringClass(type), duplicate)) { 72 | append(type.className) 73 | } 74 | } 75 | else -> { 76 | annotate(Style("keyword")) { 77 | append(type.className) 78 | } 79 | } 80 | } 81 | return this 82 | } 83 | 84 | fun BytecodePrinter.appendJavaPackageName(type: Type): BytecodePrinter { 85 | require(type.sort == Type.OBJECT) 86 | append(type.className) // do not link - package name, not class name 87 | return this 88 | } 89 | 90 | fun BytecodePrinter.appendGenericSignature(signature: String): BytecodePrinter { 91 | // TODO: link 92 | append(signature) 93 | return this 94 | } -------------------------------------------------------------------------------- /server/src/main/resources/at/yawk/javabrowser/server/view/leafArtifact.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true> 2 | <#-- @ftlvariable name="" type="at.yawk.javabrowser.server.view.LeafArtifactView" --> 3 | <#import "page.ftl" as page> 4 | <#import "declarationNode.ftl" as declarationNode> 5 | <#import "fullTextSearchForm.ftl" as ftsf> 6 | <#import "alternatives.ftl" as at> 7 | <#import "directory.ftl" as directory> 8 | <#import "diffStats.ftl" as diffStats> 9 | 10 | <#assign additionalMenu> 11 | <@at.alternatives alternatives/> 12 | 13 | <@page.page title="${path.artifact.stringId}" realm='source' newPath=path oldPath=oldPath! additionalMenu=additionalMenu narrow=true tooltip=true> 14 |
15 | <#include "metadata.ftl"> 16 | 17 | <#if oldPath??> 18 | <@diffStats.diffStats path oldPath/> 19 | 20 | 21 | <#if !oldPath??> 22 | <@ftsf.fullTextSearchForm query='' searchArtifact=path.artifact/> 23 | 30 | 31 | 32 | <#if dependencies?has_content> 33 |
34 | directory Dependencies 35 |
36 |
    37 | <#list dependencies as dependency> 38 |
  • 39 | <#if dependency.prefix??> 40 | ${dependency.prefix}${dependency.suffix} 41 | <#if dependency.aliasedTo??>(available as 42 | ${dependency.aliasedTo}) 43 |
  • 44 | 45 |
46 |
47 |
48 | 49 |
50 | directory Declarations 51 |
52 |
53 |
    54 | <#if oldPath??> 55 | <#assign diffArtifactId=oldPath.artifact.stringId> 56 | <#else> 57 | <#assign diffArtifactId=""> 58 | 59 | <@ConservativeLoopBlock iterator=topLevelPackages; package> 60 |
  • <@declarationNode.declarationNode node=package diffArtifactId=diffArtifactId/>
  • 61 | 62 |
63 |
64 |
65 |
66 |
67 | directory Java files 68 |
69 | <@directory.directory topDirectoryEntries["SOURCE"]/> 70 |
71 |
72 |
73 | directory Class files 74 |
75 | <@directory.directory topDirectoryEntries["BYTECODE"]/> 76 |
77 |
78 |
79 | -------------------------------------------------------------------------------- /generator/src/test/kotlin/at/yawk/javabrowser/generator/bytecode/ModulePrinterTest.kt: -------------------------------------------------------------------------------- 1 | package at.yawk.javabrowser.generator.bytecode 2 | 3 | import at.yawk.javabrowser.BindingId 4 | import org.objectweb.asm.Opcodes 5 | import org.objectweb.asm.tree.ModuleNode 6 | import org.testng.Assert 7 | import org.testng.annotations.Test 8 | 9 | class ModulePrinterTest { 10 | // codegen doesn't work for module-info right now, so we build our own module nodes. 11 | 12 | private fun hashBinding(binding: String) = BindingId(binding.hashCode().toLong()) 13 | 14 | @Test 15 | fun require() { 16 | val node = ModuleNode("foo.bar", 0, null) 17 | node.visitRequire("a.b", 0, null) 18 | node.visitRequire("c", Opcodes.ACC_SYNTHETIC, "version") 19 | node.visitEnd() 20 | 21 | Assert.assertEquals( 22 | BytecodePrinter(this::hashBinding).also { printModuleBody(it, node) }.finishString().trimIndent(), 23 | """ 24 | requires a.b; // flags: (0x0000) 25 | requires c@version; // flags: (0x1000) ACC_SYNTHETIC 26 | """.trimIndent() 27 | ) 28 | } 29 | 30 | @Test 31 | fun exports() { 32 | val node = ModuleNode("foo.bar", 0, null) 33 | node.visitExport("a/b", 0, "mod1", "mod2") 34 | node.visitExport("c", 0, "mod1") 35 | node.visitExport("d", 0) 36 | node.visitEnd() 37 | 38 | Assert.assertEquals( 39 | BytecodePrinter(this::hashBinding).also { printModuleBody(it, node) }.finishString().trimIndent(), 40 | """ 41 | exports a.b to // flags: (0x0000) 42 | mod1, 43 | mod2; 44 | exports c to mod1; // flags: (0x0000) 45 | exports d; // flags: (0x0000) 46 | """.trimIndent() 47 | ) 48 | } 49 | 50 | @Test 51 | fun opens() { 52 | val node = ModuleNode("foo.bar", 0, null) 53 | node.visitOpen("a/b", 0, "mod1", "mod2") 54 | node.visitOpen("c", 0, "mod1") 55 | node.visitOpen("d", 0) 56 | node.visitEnd() 57 | 58 | Assert.assertEquals( 59 | BytecodePrinter(this::hashBinding).also { printModuleBody(it, node) }.finishString().trimIndent(), 60 | """ 61 | opens a.b to // flags: (0x0000) 62 | mod1, 63 | mod2; 64 | opens c to mod1; // flags: (0x0000) 65 | opens d; // flags: (0x0000) 66 | """.trimIndent() 67 | ) 68 | } 69 | 70 | @Test 71 | fun provides() { 72 | val node = ModuleNode("foo.bar", 0, null) 73 | node.visitProvide("s/t", "a/b", "c", "d") 74 | node.visitProvide("s/t", "a/b", "c") 75 | node.visitProvide("s/t", "a/b") 76 | node.visitEnd() 77 | 78 | Assert.assertEquals( 79 | BytecodePrinter(this::hashBinding).also { printModuleBody(it, node) }.finishString().trimIndent(), 80 | """ 81 | provides s.t with 82 | a.b, 83 | c, 84 | d; 85 | provides s.t with 86 | a.b, 87 | c; 88 | provides s.t with 89 | a.b; 90 | """.trimIndent() 91 | ) 92 | } 93 | 94 | @Test 95 | fun header() { 96 | val node = ModuleNode("foo.bar", 0, "vers") 97 | node.visitMainClass("a/b") 98 | node.visitPackage("a") 99 | node.visitPackage("b/c") 100 | node.visitEnd() 101 | 102 | Assert.assertEquals( 103 | BytecodePrinter(this::hashBinding).also { printModuleAttributes(it, node) }.finishString().trimIndent(), 104 | """ 105 | Module: 106 | Name: foo.bar 107 | Version: vers 108 | flags: (0x0000) 109 | MainClass: a.b 110 | Packages: 111 | a 112 | b.c 113 | """.trimIndent() 114 | ) 115 | } 116 | } -------------------------------------------------------------------------------- /server/src/main/resources/assets/theme-default.css: -------------------------------------------------------------------------------- 1 | /* This theme matches the default intellij theme except that javadoc is blue */ 2 | 3 | .theme-default code .local-hover { 4 | background: #ddd; 5 | } 6 | 7 | .theme-default code .diff-marker { 8 | color: #000 !important; 9 | } 10 | 11 | .theme-default code .line, .theme-default code .diff-marker { 12 | color: #888 !important; 13 | } 14 | 15 | .theme-default code .keyword { 16 | color: #000080; 17 | font-weight: bold; 18 | } 19 | 20 | .theme-default code .type-variable { 21 | color: #20999D; 22 | } 23 | 24 | .theme-default code .number-literal { 25 | color: #00f; 26 | } 27 | 28 | .theme-default code .string-literal { 29 | color: #008000; 30 | } 31 | 32 | .theme-default code .annotation { 33 | color: #808000; 34 | } 35 | 36 | .theme-default code .field { 37 | color: #660E7A; 38 | font-weight: bold; 39 | } 40 | 41 | .theme-default code .field.static { 42 | font-weight: inherit; 43 | font-style: italic; 44 | } 45 | 46 | .theme-default code .comment.javadoc, .theme-default code .javadoc-rendered { 47 | color: #0033cc; 48 | border-color: #0033cc; 49 | } 50 | 51 | .theme-default code .comment { 52 | color: #808080; 53 | } 54 | 55 | .theme-default code .variable, .theme-default code a:link, .theme-default code a:visited { 56 | color: inherit; 57 | text-decoration-color: #ccc; 58 | } 59 | 60 | .theme-default a:visited, .theme-default a:link { 61 | color: blue; 62 | } 63 | 64 | .theme-default .deletion, .theme-default .decl-diff-DELETION { 65 | background-color: #FFD5CC; 66 | } 67 | 68 | .theme-default .insertion, .theme-default .decl-diff-INSERTION { 69 | background-color: #BEE6BE; 70 | } 71 | 72 | .theme-default .decl-diff-CHANGED_INTERNALLY { 73 | background-color: #CAD9FA; 74 | } 75 | 76 | .theme-default .foreground-old { 77 | color: #ff9595; 78 | } 79 | 80 | .theme-default .foreground-new { 81 | color: #83de83; 82 | } 83 | 84 | .highlight { 85 | /* Use rgba instead of a lighter, opaque tone for better mixing with local variable hover */ 86 | background-color: rgba(255, 245, 133, 0.5); 87 | } 88 | 89 | .theme-default .search-dialog { 90 | background: white; 91 | border: 1px solid #ccc; 92 | } 93 | 94 | .theme-default .fts-result { 95 | border-top-color: #ccc; 96 | } 97 | 98 | .theme-default .fts-result code { 99 | border-bottom-color: #eee; 100 | } 101 | 102 | .theme-default #header { 103 | background-color: #222; 104 | color: #AFB1B3; 105 | } 106 | 107 | .theme-default .message-box { 108 | border: 1px solid #ccc; 109 | } 110 | 111 | .theme-default .metadata { 112 | /* Some logos have a white background, so fade out before they appear. */ 113 | background: linear-gradient(to right, #eee 0%, #eee 60%, #fff 75%); 114 | } 115 | 116 | .theme-default #tooltip { 117 | background: white; 118 | border: 1px solid #ccc; 119 | } 120 | 121 | .theme-default :target::before, .theme-default :target { 122 | background-color: #64ccc9; 123 | } 124 | 125 | .theme-default .declaration-tree .selected a { 126 | background: #ccc; 127 | } 128 | 129 | .theme-default .reference-detail-table { 130 | border-color: #ccc; 131 | } 132 | 133 | .theme-default .reference-detail-table td:nth-child(even), .theme-default .reference-detail-table th:nth-child(even) { 134 | background: #eee; 135 | } 136 | 137 | .theme-default .reference-detail-list h2, .theme-default .reference-detail-list h3, .theme-default .structure > ul { 138 | background: white; 139 | } 140 | 141 | .theme-default .hit-result-limit { 142 | background: #fcc; 143 | } 144 | 145 | span.javadoc-render-toggle { 146 | background-image: url("/assets/icons/general/layoutPreviewOnly.svg"); 147 | } 148 | --------------------------------------------------------------------------------