10bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets/* 20bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * Copyright (C) 2017 The Android Open Source Project 30bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * 40bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * Licensed under the Apache License, Version 2.0 (the "License"); 50bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * you may not use this file except in compliance with the License. 60bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * You may obtain a copy of the License at 70bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * 80bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * http://www.apache.org/licenses/LICENSE-2.0 90bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * 100bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * Unless required by applicable law or agreed to in writing, software 110bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * distributed under the License is distributed on an "AS IS" BASIS, 120bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 130bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * See the License for the specific language governing permissions and 140bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets * limitations under the License. 150bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets */ 160bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 17bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viverettepackage androidx.lifecycle 180bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 19bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viveretteimport androidx.lifecycle.model.AdapterClass 20bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viveretteimport androidx.lifecycle.model.EventMethodCall 21bdc4c86d3dff74f6634a38e2f7b316b0e823a2c8Alan Viveretteimport androidx.lifecycle.model.getAdapterName 2262693d74bb01e67e62a6601c4a79ac67136a458fshepshapardimport com.squareup.javapoet.AnnotationSpec 233a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.ClassName 243a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.FieldSpec 253a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.JavaFile 263a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.MethodSpec 273a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.ParameterSpec 283a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.TypeName 293a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinetsimport com.squareup.javapoet.TypeSpec 3062693d74bb01e67e62a6601c4a79ac67136a458fshepshapardimport javax.annotation.processing.ProcessingEnvironment 310bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsimport javax.lang.model.element.Modifier 320bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsimport javax.lang.model.element.TypeElement 338dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmerimport javax.tools.StandardLocation 340bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 3562693d74bb01e67e62a6601c4a79ac67136a458fshepshapardfun writeModels(infos: List<AdapterClass>, processingEnv: ProcessingEnvironment) { 3662693d74bb01e67e62a6601c4a79ac67136a458fshepshapard infos.forEach({ writeAdapter(it, processingEnv) }) 370bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets} 380bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 3962693d74bb01e67e62a6601c4a79ac67136a458fshepshapardprivate val GENERATED_PACKAGE = "javax.annotation" 4062693d74bb01e67e62a6601c4a79ac67136a458fshepshapardprivate val GENERATED_NAME = "Generated" 410bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsprivate val LIFECYCLE_EVENT = Lifecycle.Event::class.java 420bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 430bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsprivate val T = "\$T" 440bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsprivate val N = "\$N" 450bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsprivate val L = "\$L" 4662693d74bb01e67e62a6601c4a79ac67136a458fshepshapardprivate val S = "\$S" 470bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 4891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate val OWNER_PARAM: ParameterSpec = ParameterSpec.builder( 4991df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets ClassName.get(LifecycleOwner::class.java), "owner").build() 5091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate val EVENT_PARAM: ParameterSpec = ParameterSpec.builder( 5191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets ClassName.get(LIFECYCLE_EVENT), "event").build() 5291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate val ON_ANY_PARAM: ParameterSpec = ParameterSpec.builder(TypeName.BOOLEAN, "onAny").build() 5391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 5491df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate val METHODS_LOGGER: ParameterSpec = ParameterSpec.builder( 5591df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets ClassName.get(MethodCallsLogger::class.java), "logger").build() 5691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 5791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate const val HAS_LOGGER_VAR = "hasLogger" 5891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 5962693d74bb01e67e62a6601c4a79ac67136a458fshepshapardprivate fun writeAdapter(adapter: AdapterClass, processingEnv: ProcessingEnvironment) { 6091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val receiverField: FieldSpec = FieldSpec.builder(ClassName.get(adapter.type), "mReceiver", 610bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets Modifier.FINAL).build() 6291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val dispatchMethodBuilder = MethodSpec.methodBuilder("callMethods") 630bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .returns(TypeName.VOID) 6491df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .addParameter(OWNER_PARAM) 6591df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .addParameter(EVENT_PARAM) 6691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .addParameter(ON_ANY_PARAM) 6791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .addParameter(METHODS_LOGGER) 680bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addModifiers(Modifier.PUBLIC) 690bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addAnnotation(Override::class.java) 700bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val dispatchMethod = dispatchMethodBuilder.apply { 7191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 7291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets addStatement("boolean $L = $N != null", HAS_LOGGER_VAR, METHODS_LOGGER) 7391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val callsByEventType = adapter.calls.groupBy { it.method.onLifecycleEvent.value } 7491df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets beginControlFlow("if ($N)", ON_ANY_PARAM).apply { 7591df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets writeMethodCalls(callsByEventType[Lifecycle.Event.ON_ANY] ?: emptyList(), receiverField) 7691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets }.endControlFlow() 7791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 7891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets callsByEventType 7991df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .filterKeys { key -> key != Lifecycle.Event.ON_ANY } 80619b2be303df81e1b807112100d9a0e92f43e951Sergey Vasilinets .forEach { (event, calls) -> 8191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets beginControlFlow("if ($N == $T.$L)", EVENT_PARAM, LIFECYCLE_EVENT, event) 8291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets writeMethodCalls(calls, receiverField) 8391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets endControlFlow() 840bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets } 850bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets }.build() 860bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 873a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinets val receiverParam = ParameterSpec.builder( 883a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinets ClassName.get(adapter.type), "receiver").build() 890bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 903a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinets val syntheticMethods = adapter.syntheticMethods.map { 910bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val method = MethodSpec.methodBuilder(syntheticName(it)) 920bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .returns(TypeName.VOID) 930bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addModifiers(Modifier.PUBLIC) 940bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addModifiers(Modifier.STATIC) 950bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addParameter(receiverParam) 960bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets if (it.parameters.size >= 1) { 9791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets method.addParameter(OWNER_PARAM) 980bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets } 990bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets if (it.parameters.size == 2) { 10091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets method.addParameter(EVENT_PARAM) 1010bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets } 1020bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1030bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val count = it.parameters.size 1040bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val paramString = generateParamString(count) 1050bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets method.addStatement("$N.$L($paramString)", receiverParam, it.name(), 10691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets *takeParams(count, OWNER_PARAM, EVENT_PARAM)) 1070bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets method.build() 1080bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets } 1090bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1100bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val constructor = MethodSpec.constructorBuilder() 1110bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addParameter(receiverParam) 1120bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addStatement("this.$N = $N", receiverField, receiverParam) 1130bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .build() 1140bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1153a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinets val adapterName = getAdapterName(adapter.type) 11662693d74bb01e67e62a6601c4a79ac67136a458fshepshapard val adapterTypeSpecBuilder = TypeSpec.classBuilder(adapterName) 1170bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addModifiers(Modifier.PUBLIC) 11891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets .addSuperinterface(ClassName.get(GeneratedAdapter::class.java)) 1190bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addField(receiverField) 1200bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addMethod(constructor) 1210bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addMethod(dispatchMethod) 1220bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets .addMethods(syntheticMethods) 12362693d74bb01e67e62a6601c4a79ac67136a458fshepshapard 12462693d74bb01e67e62a6601c4a79ac67136a458fshepshapard addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder, processingEnv) 12562693d74bb01e67e62a6601c4a79ac67136a458fshepshapard 12662693d74bb01e67e62a6601c4a79ac67136a458fshepshapard JavaFile.builder(adapter.type.getPackageQName(), adapterTypeSpecBuilder.build()) 12762693d74bb01e67e62a6601c4a79ac67136a458fshepshapard .build().writeTo(processingEnv.filer) 1288dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer 1298dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer generateKeepRule(adapter.type, processingEnv) 13062693d74bb01e67e62a6601c4a79ac67136a458fshepshapard} 13162693d74bb01e67e62a6601c4a79ac67136a458fshepshapard 13262693d74bb01e67e62a6601c4a79ac67136a458fshepshapardprivate fun addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder: TypeSpec.Builder, 13391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets processingEnv: ProcessingEnvironment) { 13462693d74bb01e67e62a6601c4a79ac67136a458fshepshapard val generatedAnnotationAvailable = processingEnv 13562693d74bb01e67e62a6601c4a79ac67136a458fshepshapard .elementUtils 13662693d74bb01e67e62a6601c4a79ac67136a458fshepshapard .getTypeElement(GENERATED_PACKAGE + "." + GENERATED_NAME) != null 13762693d74bb01e67e62a6601c4a79ac67136a458fshepshapard if (generatedAnnotationAvailable) { 13862693d74bb01e67e62a6601c4a79ac67136a458fshepshapard val generatedAnnotationSpec = 13962693d74bb01e67e62a6601c4a79ac67136a458fshepshapard AnnotationSpec.builder(ClassName.get(GENERATED_PACKAGE, GENERATED_NAME)).addMember( 14091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets "value", 14191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets S, 14291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets LifecycleProcessor::class.java.canonicalName).build() 14362693d74bb01e67e62a6601c4a79ac67136a458fshepshapard adapterTypeSpecBuilder.addAnnotation(generatedAnnotationSpec) 14462693d74bb01e67e62a6601c4a79ac67136a458fshepshapard } 1450bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets} 1460bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1478dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmerprivate fun generateKeepRule(type: TypeElement, processingEnv: ProcessingEnvironment) { 1488dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer val adapterClass = type.getPackageQName() + "." + getAdapterName(type) 1498dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer val observerClass = type.toString() 1508dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer val keepRule = """# Generated keep rule for Lifecycle observer adapter. 1512196a6113412dc0f2c54ad9d2fe47ab013f56852Adam Koski |-if class $observerClass { 1522196a6113412dc0f2c54ad9d2fe47ab013f56852Adam Koski | <init>(...); 153eb9bc738f21a41021d236d0e30c3034d3163e613Sergey Vasilinets |} 154eb9bc738f21a41021d236d0e30c3034d3163e613Sergey Vasilinets |-keep class $adapterClass { 155eb9bc738f21a41021d236d0e30c3034d3163e613Sergey Vasilinets | <init>(...); 156eb9bc738f21a41021d236d0e30c3034d3163e613Sergey Vasilinets |} 1578dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer |""".trimMargin() 1588dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer 1598dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer // Write the keep rule to the META-INF/proguard directory of the Jar file. The file name 1608dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer // contains the fully qualified observer name so that file names are unique. This will allow any 1618dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer // jar file merging to not overwrite keep rule files. 1628dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer val path = "META-INF/proguard/$observerClass.pro" 1638dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer val out = processingEnv.filer.createResource(StandardLocation.CLASS_OUTPUT, "", path) 1648dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer out.openWriter().use { it.write(keepRule) } 1658dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer} 1668dd2658f5c9c58c197b7d02d8214af22a5de60abShane Farmer 16791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinetsprivate fun MethodSpec.Builder.writeMethodCalls(calls: List<EventMethodCall>, 1680bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets receiverField: FieldSpec) { 1693a9bd3acec72820f173ebf52a1cb7bb1d52eeabfSergey Vasilinets calls.forEach { (method, syntheticAccess) -> 1700bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets val count = method.method.parameters.size 17191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val callType = 1 shl count 17291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val methodName = method.method.name() 17391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets beginControlFlow("if (!$L || $N.approveCall($S, $callType))", 17491df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets HAS_LOGGER_VAR, METHODS_LOGGER, methodName).apply { 17591df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets 17691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets if (syntheticAccess == null) { 17791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val paramString = generateParamString(count) 17891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets addStatement("$N.$L($paramString)", receiverField, 17991df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets methodName, 18091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets *takeParams(count, OWNER_PARAM, EVENT_PARAM)) 18191df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets } else { 18291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val originalType = syntheticAccess 18391df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val paramString = generateParamString(count + 1) 18491df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets val className = ClassName.get(originalType.getPackageQName(), 18591df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets getAdapterName(originalType)) 18691df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets addStatement("$T.$L($paramString)", className, 18791df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets syntheticName(method.method), 18891df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets *takeParams(count + 1, receiverField, OWNER_PARAM, EVENT_PARAM)) 18991df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets } 19091df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets }.endControlFlow() 1910bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets } 19291df2cf6dacb3035df3f6b98e895cdc9f025553aSergey Vasilinets addStatement("return") 1930bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets} 1940bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1950bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinetsprivate fun takeParams(count: Int, vararg params: Any) = params.take(count).toTypedArray() 1960bbdd57e9654789f419177f1ff90221d5872b116Sergey Vasilinets 1972196a6113412dc0f2c54ad9d2fe47ab013f56852Adam Koskiprivate fun generateParamString(count: Int) = (0 until count).joinToString(",") { N } 198