diff --git a/.gitignore b/.gitignore index 0e4d2755b..261ef345b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ TNoodle-WCA-*.jar TNoodle-Build-latest.jar tnoodle_resources/ tnoodle_pruning_cache/ -**/logging/*.log +**/tnoodle.log version.tnoodle *.hprof diff --git a/build.gradle.kts b/build.gradle.kts index c98f2a414..66a35f602 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ buildscript { exclude(group = "com.android.tools.build") } classpath(GOOGLE_APPENGINE_GRADLE) + classpath(KOTLINX_ATOMICFU_GRADLE) } } diff --git a/buildSrc/src/main/kotlin/PluginsHack.kt b/buildSrc/src/main/kotlin/PluginsHack.kt index a8e302ca0..49992511c 100644 --- a/buildSrc/src/main/kotlin/PluginsHack.kt +++ b/buildSrc/src/main/kotlin/PluginsHack.kt @@ -1,6 +1,7 @@ import dependencies.Libraries.Buildscript.GOOGLE_APPENGINE_GRADLE_ACTUAL import dependencies.Libraries.Buildscript.PROGUARD_GRADLE_ACTUAL import dependencies.Libraries.Buildscript.WCA_I18N_ACTUAL +import dependencies.Libraries.Buildscript.KOTLINX_ATOMICFU_GRADLE_ACTUAL import dependencies.Plugins.DEPENDENCY_VERSIONS_ACTUAL import dependencies.Plugins.GIT_VERSION_TAG_ACTUAL import dependencies.Plugins.GOOGLE_APPENGINE_ACTUAL @@ -8,6 +9,7 @@ import dependencies.Plugins.KOTLESS_ACTUAL import dependencies.Plugins.KOTLIN_JVM_ACTUAL import dependencies.Plugins.KOTLIN_MULTIPLATFORM_ACTUAL import dependencies.Plugins.KOTLIN_SERIALIZATION_ACTUAL +import dependencies.Plugins.KOTLINX_ATOMICFU_ACTUAL import dependencies.Plugins.SHADOW_ACTUAL import dependencies.Plugins.NODEJS_ACTUAL @@ -47,6 +49,9 @@ inline val PluginDependenciesSpec.GIT_VERSION_TAG: PluginDependencySpec inline val PluginDependenciesSpec.KOTLESS: PluginDependencySpec get() = KOTLESS_ACTUAL +inline val PluginDependenciesSpec.KOTLINX_ATOMICFU: PluginDependencySpec + get() = KOTLINX_ATOMICFU_ACTUAL + inline val PROGUARD_GRADLE: String get() = PROGUARD_GRADLE_ACTUAL @@ -55,3 +60,6 @@ inline val WCA_I18N: String inline val GOOGLE_APPENGINE_GRADLE: String get() = GOOGLE_APPENGINE_GRADLE_ACTUAL + +inline val KOTLINX_ATOMICFU_GRADLE: String + get() = KOTLINX_ATOMICFU_GRADLE_ACTUAL diff --git a/buildSrc/src/main/kotlin/dependencies/Libraries.kt b/buildSrc/src/main/kotlin/dependencies/Libraries.kt index 84ef6ea65..9dc545fd6 100644 --- a/buildSrc/src/main/kotlin/dependencies/Libraries.kt +++ b/buildSrc/src/main/kotlin/dependencies/Libraries.kt @@ -5,6 +5,7 @@ object Libraries { val ZIP4J = "net.lingala.zip4j:zip4j:${Versions.ZIP4J}" val ITEXTPDF = "com.itextpdf:itextpdf:${Versions.ITEXTPDF}" val BATIK_TRANSCODER = "org.apache.xmlgraphics:batik-transcoder:${Versions.BATIK_TRANSCODER}" + val BATIK_CODEC = "org.apache.xmlgraphics:batik-codec:${Versions.BATIK_CODEC}" val SNAKEYAML = "org.yaml:snakeyaml:${Versions.SNAKEYAML}" val SYSTEM_TRAY = "com.dorkbox:SystemTray:${Versions.SYSTEM_TRAY}" val BOUNCYCASTLE = "org.bouncycastle:bcprov-jdk15on:${Versions.BOUNCYCASTLE}" @@ -27,10 +28,12 @@ object Libraries { val APACHE_COMMONS_LANG3 = "org.apache.commons:commons-lang3:${Versions.APACHE_COMMONS_LANG3}" val KOTLESS_KTOR = "io.kotless:ktor-lang:${Versions.KOTLESS_KTOR}" val TESTING_MOCKK = "io.mockk:mockk:${Versions.TESTING_MOCKK}" + val KOTLINX_ATOMICFU_GRADLE = "org.jetbrains.kotlinx:atomicfu-gradle-plugin:${Versions.KOTLINX_ATOMICFU_GRADLE}" object Buildscript { val PROGUARD_GRADLE_ACTUAL = PROGUARD_GRADLE val WCA_I18N_ACTUAL = WCA_I18N val GOOGLE_APPENGINE_GRADLE_ACTUAL = GOOGLE_APPENGINE_GRADLE + val KOTLINX_ATOMICFU_GRADLE_ACTUAL = KOTLINX_ATOMICFU_GRADLE } } diff --git a/buildSrc/src/main/kotlin/dependencies/Plugins.kt b/buildSrc/src/main/kotlin/dependencies/Plugins.kt index 51b914b8e..6e18d5a07 100644 --- a/buildSrc/src/main/kotlin/dependencies/Plugins.kt +++ b/buildSrc/src/main/kotlin/dependencies/Plugins.kt @@ -26,10 +26,13 @@ object Plugins { inline val PluginDependenciesSpec.GIT_VERSION_TAG_ACTUAL: PluginDependencySpec get() = id("com.palantir.git-version").version(Versions.Plugins.GIT_VERSION_TAG) + inline val PluginDependenciesSpec.KOTLESS_ACTUAL: PluginDependencySpec + get() = id("io.kotless").version(Versions.Plugins.KOTLESS) + // not versioned because the classpath from the buildscript {} block already implies a version inline val PluginDependenciesSpec.GOOGLE_APPENGINE_ACTUAL: PluginDependencySpec get() = id("com.google.cloud.tools.appengine") - inline val PluginDependenciesSpec.KOTLESS_ACTUAL: PluginDependencySpec - get() = id("io.kotless").version(Versions.Plugins.KOTLESS) + inline val PluginDependenciesSpec.KOTLINX_ATOMICFU_ACTUAL: PluginDependencySpec + get() = id("kotlinx-atomicfu") } diff --git a/buildSrc/src/main/kotlin/dependencies/Versions.kt b/buildSrc/src/main/kotlin/dependencies/Versions.kt index a9d57be16..ac15813f4 100644 --- a/buildSrc/src/main/kotlin/dependencies/Versions.kt +++ b/buildSrc/src/main/kotlin/dependencies/Versions.kt @@ -12,6 +12,7 @@ object Versions { val ZIP4J = "2.6.3" val ITEXTPDF = "5.5.13.2" val BATIK_TRANSCODER = BATIK + val BATIK_CODEC = BATIK val SNAKEYAML = "1.27" val SYSTEM_TRAY = "3.17" val BOUNCYCASTLE = "1.66" @@ -34,6 +35,7 @@ object Versions { val APACHE_COMMONS_LANG3 = "3.11" val KOTLESS_KTOR = KOTLESS val TESTING_MOCKK = "1.10.2" + val KOTLINX_ATOMICFU_GRADLE = "0.14.4" object Plugins { val SHADOW = "6.0.0" diff --git a/cloudscrambles/build.gradle.kts b/cloudscrambles/build.gradle.kts index 9a702273b..9bfc2a054 100644 --- a/cloudscrambles/build.gradle.kts +++ b/cloudscrambles/build.gradle.kts @@ -3,6 +3,7 @@ import configurations.Languages.attachRemoteRepositories import configurations.ProjectVersions.tNoodleImplOrDefault import configurations.ProjectVersions.tNoodleVersionOrDefault import dependencies.Libraries.BATIK_TRANSCODER +import dependencies.Libraries.BATIK_CODEC import dependencies.Libraries.GOOGLE_CLOUD_STORAGE import dependencies.Libraries.KOTLESS_KTOR import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -26,6 +27,8 @@ dependencies { //implementation(KOTLESS_KTOR) implementation(GOOGLE_CLOUD_STORAGE) implementation(BATIK_TRANSCODER) + + runtimeOnly(BATIK_CODEC) } tasks.withType { diff --git a/cloudscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/cloudscrambles/routing/ScrambleViewHandler.kt b/cloudscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/cloudscrambles/routing/ScrambleViewHandler.kt index c6ed10f24..8ccd5780d 100644 --- a/cloudscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/cloudscrambles/routing/ScrambleViewHandler.kt +++ b/cloudscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/cloudscrambles/routing/ScrambleViewHandler.kt @@ -9,22 +9,19 @@ import io.ktor.response.respondText import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.route -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.apache.batik.anim.dom.SVGDOMImplementation import org.apache.batik.transcoder.TranscoderInput import org.apache.batik.transcoder.TranscoderOutput import org.apache.batik.transcoder.TranscodingHints import org.apache.batik.transcoder.image.ImageTranscoder +import org.apache.batik.transcoder.image.PNGTranscoder import org.apache.batik.util.SVGConstants import org.worldcubeassociation.tnoodle.scrambles.Puzzle import org.worldcubeassociation.tnoodle.server.RouteHandler import org.worldcubeassociation.tnoodle.server.cloudscrambles.serial.PuzzleImageJsonData import org.worldcubeassociation.tnoodle.server.model.PuzzleData import org.worldcubeassociation.tnoodle.svglite.Svg -import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream -import javax.imageio.ImageIO object ScrambleViewHandler : RouteHandler { private const val PUZZLE_KEY_PARAM = PuzzleListHandler.PUZZLE_KEY_PARAM @@ -32,20 +29,6 @@ object ScrambleViewHandler : RouteHandler { private const val QUERY_SCRAMBLE_PARAM = "scramble" private const val QUERY_COLOR_SCHEME_PARAM = "scheme" - // Copied from http://bbgen.net/blog/2011/06/java-svg-to-bufferedimage/ - internal class BufferedImageTranscoder : ImageTranscoder() { - var bufferedImage: BufferedImage? = null - private set - - override fun createImage(w: Int, h: Int): BufferedImage { - return BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB) - } - - override fun writeImage(img: BufferedImage, output: TranscoderOutput) { - this.bufferedImage = img - } - } - private suspend fun ApplicationCall.withScramble(handle: suspend ApplicationCall.(Puzzle, Svg) -> Unit) { val name = parameters[PUZZLE_KEY_PARAM] ?: return respondText("Please specify a puzzle") @@ -67,37 +50,32 @@ object ScrambleViewHandler : RouteHandler { route("{$PUZZLE_KEY_PARAM}") { get("png") { call.withScramble { puzzle, svg -> - val svgFile = svg.toString().byteInputStream() - val (width, height) = puzzle.preferredSize.let { it.width to it.height } - val imageTranscoder = BufferedImageTranscoder() // Copied from http://stackoverflow.com/a/6634963 // with some tweaks. - val impl = SVGDOMImplementation.getDOMImplementation() - val hints = TranscodingHints().apply { this[ImageTranscoder.KEY_WIDTH] = width.toFloat() this[ImageTranscoder.KEY_HEIGHT] = height.toFloat() - this[ImageTranscoder.KEY_DOM_IMPLEMENTATION] = impl + this[ImageTranscoder.KEY_DOM_IMPLEMENTATION] = SVGDOMImplementation.getDOMImplementation() this[ImageTranscoder.KEY_DOCUMENT_ELEMENT_NAMESPACE_URI] = SVGConstants.SVG_NAMESPACE_URI this[ImageTranscoder.KEY_DOCUMENT_ELEMENT_NAMESPACE_URI] = SVGConstants.SVG_NAMESPACE_URI this[ImageTranscoder.KEY_DOCUMENT_ELEMENT] = SVGConstants.SVG_SVG_TAG this[ImageTranscoder.KEY_XML_PARSER_VALIDATING] = false } - imageTranscoder.transcodingHints = hints + val imageTranscoder = PNGTranscoder().apply { + transcodingHints = hints + } + val svgFile = svg.toString().byteInputStream() val input = TranscoderInput(svgFile) - imageTranscoder.transcode(input, TranscoderOutput()) - - val img = imageTranscoder.bufferedImage - val bytes = ByteArrayOutputStream().also { - withContext(Dispatchers.IO) { ImageIO.write(img, "png", it) } - } + val outputBytes = ByteArrayOutputStream() + val output = TranscoderOutput(outputBytes) + imageTranscoder.transcode(input, output) - respondBytes(bytes.toByteArray(), ContentType.Image.PNG) + respondBytes(outputBytes.toByteArray(), ContentType.Image.PNG) } } get("svg") { diff --git a/tnoodle-server/build.gradle.kts b/tnoodle-server/build.gradle.kts index f3111e6c3..c91207a2a 100644 --- a/tnoodle-server/build.gradle.kts +++ b/tnoodle-server/build.gradle.kts @@ -20,6 +20,7 @@ attachRemoteRepositories() plugins { kotlin("jvm") KOTLIN_SERIALIZATION + KOTLINX_ATOMICFU } dependencies { diff --git a/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/model/cache/CoroutineScrambleCacher.kt b/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/model/cache/CoroutineScrambleCacher.kt index e0c55094c..66ab480f5 100644 --- a/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/model/cache/CoroutineScrambleCacher.kt +++ b/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/model/cache/CoroutineScrambleCacher.kt @@ -1,5 +1,6 @@ package org.worldcubeassociation.tnoodle.server.model.cache +import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import kotlinx.coroutines.channels.produce import org.worldcubeassociation.tnoodle.scrambles.Puzzle @@ -8,9 +9,6 @@ import java.util.concurrent.ThreadFactory import kotlin.coroutines.CoroutineContext class CoroutineScrambleCacher(val puzzle: Puzzle, capacity: Int) : CoroutineScope { - var available: Int = 0 - private set - override val coroutineContext: CoroutineContext get() = JOB_CONTEXT @@ -18,12 +16,17 @@ class CoroutineScrambleCacher(val puzzle: Puzzle, capacity: Int) : CoroutineScop private val buffer = produce(capacity = capacity) { while (true) { send(puzzle.generateScramble()) - .also { synchronized(available) { available++ } } + .also { size += 1 } } } + private val size = atomic(0) + + val available: Int + get() = size.value + suspend fun yieldScramble(): String = buffer.receive() - .also { synchronized(available) { available-- } } + .also { size -= 1 } fun getScramble(): String = runBlocking { yieldScramble() } diff --git a/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/routing/VersionHandler.kt b/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/routing/VersionHandler.kt index 388a5ecba..76e83cd52 100644 --- a/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/routing/VersionHandler.kt +++ b/tnoodle-server/src/main/kotlin/org/worldcubeassociation/tnoodle/server/routing/VersionHandler.kt @@ -4,10 +4,8 @@ import io.ktor.application.call import io.ktor.response.respond import io.ktor.routing.Route import io.ktor.routing.get -import kotlinx.serialization.json.json import org.worldcubeassociation.tnoodle.server.RouteHandler -import org.worldcubeassociation.tnoodle.server.crypto.AsymmetricCipher import org.worldcubeassociation.tnoodle.server.ServerEnvironmentConfig import org.worldcubeassociation.tnoodle.server.serial.VersionInfo diff --git a/tnoodle-server/src/main/resources/logback.xml b/tnoodle-server/src/main/resources/logback.xml index 7310ab455..a212395be 100644 --- a/tnoodle-server/src/main/resources/logback.xml +++ b/tnoodle-server/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - logging/tnoodle.log + tnoodle.log %date %level [%thread] %logger{10} [%file:%line] %msg%n diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/WebscramblesServer.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/WebscramblesServer.kt index 8a8a40880..de00ac982 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/WebscramblesServer.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/WebscramblesServer.kt @@ -85,7 +85,7 @@ class WebscramblesServer(val environmentConfig: ServerEnvironmentConfig) : Appli val desiredJsEnv by parser.adding("--jsenv", help = "Add entry to global js object TNOODLE_ENV in /env.js. Treated as strings, so FOO=42 will create the entry TNOODLE_ENV['FOO'] = '42';") - val cliPort by parser.storing("-p", "--port", help = "Start TNoodle on given port. Should be numeric. Defaults to ${OfflineJarUtils.TNOODLE_PORT}", transform = { toInt() }) + val cliPort by parser.storing("-p", "--port", help = "Start TNoodle on given port. Should be numeric. Defaults to ${OfflineJarUtils.TNOODLE_PORT}", transform = String::toInt) .default(OfflineJarUtils.TNOODLE_PORT) val noBrowser by parser.flagging("-n", "--nobrowser", help = "Don't open the browser when starting the server") @@ -96,15 +96,11 @@ class WebscramblesServer(val environmentConfig: ServerEnvironmentConfig) : Appli val port = System.getenv("PORT")?.takeIf { onlineConfig }?.toIntOrNull() ?: cliPort val offlineHandler = OfflineJarUtils(port) - offlineHandler.setApplicationIcon() - - if (!noReexec) { + val isWrapped = if (!noReexec) { MainLauncher.wrapMain(args, MIN_HEAP_SIZE_MEGS) + } else false - // This second call to setApplicationIcon() is intentional. - // We want different icons for the parent and child processes. - offlineHandler.setApplicationIcon() - } + offlineHandler.setApplicationIcon(isWrapped) for (jsEnv in desiredJsEnv) { val (key, strValue) = jsEnv.split("=", limit = 2) @@ -113,7 +109,7 @@ class WebscramblesServer(val environmentConfig: ServerEnvironmentConfig) : Appli if (!noUpgrade) { try { - val shutdownMaybe = URL("http://localhost:$port${TNoodleServer.KILL_URL}").openStream() + val shutdownMaybe = URL("${offlineHandler.url}${TNoodleServer.KILL_URL}").openStream() shutdownMaybe.close() } catch (ignored: IOException) { // NOOP. This means we couldn't connect to localhost:$PORT @@ -129,8 +125,11 @@ class WebscramblesServer(val environmentConfig: ServerEnvironmentConfig) : Appli LOG.info("${LocalServerEnvironmentConfig.title} started") - val url = offlineHandler.openTabInBrowser(!noBrowser) - LOG.info("Visit $url for a readme and demo.") + if (!noBrowser) { + offlineHandler.openTabInBrowser() + } else { + LOG.info("Visit ${offlineHandler.url} for a readme and demo.") + } } } } diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/BasePdfSheet.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/BasePdfSheet.kt index 2ad6fce29..76322b753 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/BasePdfSheet.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/BasePdfSheet.kt @@ -8,10 +8,14 @@ import java.io.ByteArrayOutputStream abstract class BasePdfSheet : PdfContent { open fun openDocument() = Document() - private var renderingCache: ByteArray? = null + private val renderingCache by lazy { directRender(null) } override fun render(password: String?): ByteArray { - return renderingCache?.takeIf { password == null } ?: directRender(password) + if (password == null) { + return renderingCache + } + + return directRender(password) } private fun directRender(password: String?): ByteArray { @@ -29,7 +33,6 @@ abstract class BasePdfSheet : PdfContent { pdfDocument.close() return this.finalise(pdfBytes, password) - .also { if (password == null) renderingCache = it } } abstract fun W.writeContents(document: Document) diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/FmcSolutionSheet.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/FmcSolutionSheet.kt index 2635da242..fbf14bb69 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/FmcSolutionSheet.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/FmcSolutionSheet.kt @@ -362,9 +362,10 @@ open class FmcSolutionSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode companion object { const val FMC_LINE_THICKNESS = 0.5f - const val UNDERLINE_THICKNESS = 0.2f + const val LEADING_MULTIPLIER = 1.3f + const val FORM_TEMPLATE_WCA_ID = "WCA ID: __ __ __ __ __ __ __ __ __ __" const val SHORT_FILL = ": ____" @@ -374,7 +375,5 @@ open class FmcSolutionSheet(scrambleSet: ScrambleSet, activityCode: ActivityCode val WCA_ROTATIONS = arrayOf("x", "y", "z", "", "", "") val DIRECTION_MODIFIERS = arrayOf("", "'", "2") - - var LEADING_MULTIPLIER = 1.3f } } diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/MergedPdfWithOutline.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/MergedPdfWithOutline.kt index 475109273..dcf88c3e4 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/MergedPdfWithOutline.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/MergedPdfWithOutline.kt @@ -2,42 +2,39 @@ package org.worldcubeassociation.tnoodle.server.webscrambles.pdf import com.itextpdf.text.Document import com.itextpdf.text.pdf.* +import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.OutlineConfiguration import java.io.ByteArrayOutputStream -class MergedPdfWithOutline(val toMerge: List, val configuration: List>) : BasePdfSheet() { +class MergedPdfWithOutline(val toMerge: List, val configuration: List) : BasePdfSheet() { override fun Document.getWriter(bytes: ByteArrayOutputStream): PdfSmartCopy = PdfSmartCopy(this, bytes) override fun PdfSmartCopy.writeContents(document: Document) { val root = directContent.rootOutline - val outlineByPuzzle = mutableMapOf() - var pages = 1 - - for ((origPdf, configData) in toMerge.zip(configuration)) { - val (title, group, copies) = configData - - val d = PdfDestination(PdfDestination.FIT) - val action = PdfAction.gotoLocalPage(pages, d, this) + for ((origPdf, config) in toMerge.zip(configuration)) { + val action = PdfAction.gotoLocalPage(currentPageNumber, DESTINATION, this) - val puzzleLink: PdfOutline = outlineByPuzzle.getOrPut(group) { - PdfOutline(root, action, group, false) + val puzzleLink: PdfOutline = outlineByPuzzle.getOrPut(config.group) { + PdfOutline(root, action, config.group, false) } // Yes, invoking the constructor is enough to *add* the outline to the document. // We should REALLY get rid of itext5. - PdfOutline(puzzleLink, action, title) + PdfOutline(puzzleLink, action, config.title) val pdfReader = PdfReader(origPdf.render()) - repeat(copies) { + repeat(config.copies) { for (pageN in 1..pdfReader.numberOfPages) { val page = getImportedPage(pdfReader, pageN) addPage(page) - - pages++ } } } } + + companion object { + private val DESTINATION = PdfDestination(PdfDestination.FIT) + } } diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/util/OutlineConfiguration.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/util/OutlineConfiguration.kt new file mode 100644 index 000000000..934a0201f --- /dev/null +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf/util/OutlineConfiguration.kt @@ -0,0 +1,3 @@ +package org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util + +data class OutlineConfiguration(val title: String, val group: String, val copies: Int) diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/MainLauncher.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/MainLauncher.kt index c496c9480..db900e9dd 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/MainLauncher.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/MainLauncher.kt @@ -12,15 +12,6 @@ object MainLauncher { const val NO_REEXEC_OPT = "--noReexec" - var processType = ProcessType.UNKNOWN - private set - - enum class ProcessType { - UNKNOWN, - WRAPPER, - WORKER - } - /* * Windows doesn't give good names for java programs in the task manager, * they all just show up as instances of java.exe. @@ -31,12 +22,11 @@ object MainLauncher { * minHeapSizeMegs mb of heap space, and if not, reexecs itself and passes * an appropriate -Xmx to the jvm. */ - fun wrapMain(args: Array, minHeapSizeMegs: Int, name: String? = null) { + fun wrapMain(args: Array, minHeapSizeMegs: Int, name: String? = null): Boolean { LOG.trace("Entering ${MainLauncher::class.java}, method wrapMain, args ${args + minHeapSizeMegs.toString()}") if (NO_REEXEC_OPT in args) { - processType = ProcessType.WORKER - return + return false } val t = Thread.currentThread() @@ -59,11 +49,8 @@ object MainLauncher { val needsReExecing = needsHeapSizeReExec || needsJvmReExec LOG.info("needsReExecing: $needsReExecing") - if (needsReExecing) { - processType = ProcessType.WRAPPER - } else { - processType = ProcessType.WORKER - return + if (!needsReExecing) { + return false } // Fortunately, classpath contains our jar file if we were run @@ -95,6 +82,8 @@ object MainLauncher { } catch (e: IOException) { LOG.warn("Starting the child process failed!", e) } + + return true } private fun detectJVM(os: String, name: String?, mainClass: String): Pair { diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/OfflineJarUtils.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/OfflineJarUtils.kt index 644c30331..bf0051f29 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/OfflineJarUtils.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/server/OfflineJarUtils.kt @@ -11,48 +11,37 @@ import java.net.URISyntaxException import kotlin.system.exitProcess data class OfflineJarUtils(val port: Int) { - fun openTabInBrowser(browse: Boolean): String { - val url = "http://localhost:$port" - - if (browse) { - if (Desktop.isDesktopSupported()) { - val d = Desktop.getDesktop() - - if (d.isSupported(Desktop.Action.BROWSE)) { - try { - val uri = URI(url) - LOG.info("Attempting to open $uri in browser.") - d.browse(uri) - } catch (e: URISyntaxException) { - LOG.warn("Could not convert $url to URI", e) - } catch (e: IOException) { - LOG.warn("Error opening tab in browser", e) - } - - } else { - LOG.error("Sorry, it appears the Desktop api is supported on your platform, but the BROWSE action is not.") + val url = "http://localhost:$port" + + fun openTabInBrowser() { + if (Desktop.isDesktopSupported()) { + val d = Desktop.getDesktop() + + if (d.isSupported(Desktop.Action.BROWSE)) { + try { + val uri = URI(url) + LOG.info("Attempting to open $uri in browser.") + d.browse(uri) + } catch (e: URISyntaxException) { + LOG.warn("Could not convert $url to URI", e) + } catch (e: IOException) { + LOG.warn("Error opening tab in browser", e) } + } else { - LOG.error("Sorry, it appears the Desktop api is not supported on your platform.") + LOG.error("Sorry, it appears the Desktop api is supported on your platform, but the BROWSE action is not.") } + } else { + LOG.error("Sorry, it appears the Desktop api is not supported on your platform.") } - - return url } /* - * Sets the dock icon in OSX. Could be made to have uses in other operating systems. + * Sets the dock icon in the operating system. */ - fun setApplicationIcon() { + fun setApplicationIcon(isWrapper: Boolean = false) { // Find out which icon to use. - val processType = MainLauncher.processType - - val iconFileName = if (processType === MainLauncher.ProcessType.WORKER) ICON_WORKER else ICON_WRAPPER - - if (iconFileName != ICON_WORKER) { - // Only want to create one tray icon. - return - } + val iconFileName = if (isWrapper) ICON_WRAPPER else ICON_WORKER val trayAdapter = SystemTray.get() @@ -61,7 +50,7 @@ data class OfflineJarUtils(val port: Int) { return } - val openItem = MenuItem("Open") { openTabInBrowser(true) } + val openItem = MenuItem("Open") { openTabInBrowser() } trayAdapter.menu.add(openItem) val exitItem = MenuItem("Exit") { diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/wcif/WCIFDataBuilder.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/wcif/WCIFDataBuilder.kt index c11eb2552..e1b2609d4 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/wcif/WCIFDataBuilder.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/wcif/WCIFDataBuilder.kt @@ -3,6 +3,7 @@ package org.worldcubeassociation.tnoodle.server.webscrambles.wcif import org.worldcubeassociation.tnoodle.server.model.EventData import org.worldcubeassociation.tnoodle.server.webscrambles.Translate import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.* +import org.worldcubeassociation.tnoodle.server.webscrambles.pdf.util.OutlineConfiguration import org.worldcubeassociation.tnoodle.server.webscrambles.wcif.model.* import org.worldcubeassociation.tnoodle.server.webscrambles.wcif.model.extension.* import org.worldcubeassociation.tnoodle.server.webscrambles.zip.CompetitionZippingData @@ -122,7 +123,7 @@ object WCIFDataBuilder { } val configurations = scrambleRequests.map { - Triple(it.compileTitleString(locale, false), it.activityCode.eventModel?.description.orEmpty(), it.numCopies) + OutlineConfiguration(it.compileTitleString(locale, false), it.activityCode.eventModel?.description.orEmpty(), it.numCopies) } return MergedPdfWithOutline(originalPdfs, configurations) diff --git a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/zip/model/ZipArchive.kt b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/zip/model/ZipArchive.kt index 38c9a01f1..9515cac0e 100644 --- a/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/zip/model/ZipArchive.kt +++ b/webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/zip/model/ZipArchive.kt @@ -13,10 +13,14 @@ class ZipArchive(private val entries: List) { val allFiles: List get() = Folder.flattenFiles(entries) - private var zippingCache: ByteArray? = null + private val zippingCache by lazy { directCompress(null) } fun compress(password: String? = null): ByteArray { - return zippingCache?.takeIf { password == null } ?: directCompress(password) + if (password == null) { + return zippingCache + } + + return directCompress(password) } fun directCompress(password: String?): ByteArray { @@ -39,7 +43,6 @@ class ZipArchive(private val entries: List) { zipOut.close() return baosZip.toByteArray() - .also { if (password == null) zippingCache = it } } companion object {