Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ dependencies {

test {
inputs.files(file("$rootDir/tests/src"))
environment "MOTIF_OBSERVE", "1"
}
55 changes: 51 additions & 4 deletions compiler/src/main/kotlin/motif/compiler/JavaCodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.uber.xprocessing.ext.withRawTypeFix
import javax.lang.model.element.Modifier
import javax.lang.model.type.DeclaredType
import motif.internal.None
import motif.observe.MotifObserver

object JavaCodeGenerator {

Expand All @@ -47,12 +48,17 @@ object JavaCodeGenerator {
objectsField?.let { addField(it.spec()) }
addField(dependenciesField.spec())
cacheFields.forEach { addField(it.spec(useNullFieldInitialization)) }
addMethod(constructor.spec())
val scopeNameString = superClassName.j.toString()
addMethod(constructor.spec(shouldGenerateObserverCode, scopeNameString))
alternateConstructor?.let { addMethod(it.spec()) }
accessMethodImpls.forEach { addMethod(it.spec()) }
childMethodImpls.forEach { addMethod(it.spec()) }
addMethod(scopeProviderMethod.spec())
factoryProviderMethods.forEach { addMethods(it.specs(useNullFieldInitialization)) }
factoryProviderMethods.forEach {
addMethods(
it.specs(useNullFieldInitialization, shouldGenerateObserverCode, scopeNameString),
)
}
dependencyProviderMethods.forEach { addMethod(it.spec()) }
dependencies?.let { addType(it.spec()) }
objectsImpl?.let { addType(it.spec()) }
Expand Down Expand Up @@ -96,6 +102,22 @@ object JavaCodeGenerator {
.addStatement("this.\$N = \$N", dependenciesFieldName, dependenciesParameterName)
.build()

private fun Constructor.spec(shouldGenerateObserverCode: Boolean, scopeName: String): MethodSpec =
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(dependenciesClassName.j, dependenciesParameterName)
.apply {
if (shouldGenerateObserverCode) {
addStatement(
"\$T.notifyScopeInitializing(\$S)",
MotifObserver::class.java,
scopeName,
)
}
addStatement("this.\$N = \$N", dependenciesFieldName, dependenciesParameterName)
}
.build()

private fun AlternateConstructor.spec(): MethodSpec =
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
Expand Down Expand Up @@ -168,11 +190,36 @@ object JavaCodeGenerator {
private fun ScopeProviderMethod.spec(): MethodSpec =
MethodSpec.methodBuilder(name).returns(scopeClassName.j).addStatement("return this").build()

private fun FactoryProviderMethod.specs(useNullFieldInitialization: Boolean): List<MethodSpec> {
private fun FactoryProviderMethod.specs(
useNullFieldInitialization: Boolean,
shouldGenerateObserverCode: Boolean,
scopeName: String,
): List<MethodSpec> {
val primarySpec =
MethodSpec.methodBuilder(name)
.returns(returnTypeName.j)
.addStatement(body.spec(useNullFieldInitialization))
.apply {
if (shouldGenerateObserverCode) {
addStatement(
"\$T.notifyProvideStart(\$S, \$S)",
MotifObserver::class.java,
scopeName,
name,
)
beginControlFlow("try")
addStatement(body.spec(useNullFieldInitialization))
nextControlFlow("finally")
addStatement(
"\$T.notifyProvideComplete(\$S, \$S)",
MotifObserver::class.java,
scopeName,
name,
)
endControlFlow()
} else {
addStatement(body.spec(useNullFieldInitialization))
}
}
.build()
val spreadSpecs = spreadProviderMethods.map { it.spec() }
return listOf(primarySpec) + spreadSpecs
Expand Down
50 changes: 45 additions & 5 deletions compiler/src/main/kotlin/motif/compiler/KotlinCodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview
import com.squareup.kotlinpoet.javapoet.toKClassName
import motif.internal.None
import motif.observe.MotifObserver

@OptIn(KotlinPoetJavaPoetPreview::class)
object KotlinCodeGenerator {
Expand All @@ -50,7 +51,8 @@ object KotlinCodeGenerator {
objectsField?.let { addProperty(it.spec()) }
addProperty(dependenciesField.spec())
cacheFields.forEach { addProperty(it.spec(useNullFieldInitialization)) }
primaryConstructor(constructor.spec())
val scopeNameString = superClassName.kt.toString()
primaryConstructor(constructor.spec(shouldGenerateObserverCode, scopeNameString))
alternateConstructor?.let { addFunction(it.spec()) }
accessMethodImpls
.filter { !it.overriddenMethod.isSynthetic }
Expand All @@ -60,7 +62,11 @@ object KotlinCodeGenerator {
.forEach { addProperty(it.propSpec()) }
childMethodImpls.forEach { addFunction(it.spec()) }
addFunction(scopeProviderMethod.spec())
factoryProviderMethods.forEach { addFunctions(it.specs(useNullFieldInitialization)) }
factoryProviderMethods.forEach {
addFunctions(
it.specs(useNullFieldInitialization, shouldGenerateObserverCode, scopeNameString),
)
}
dependencyProviderMethods.forEach { addFunction(it.spec()) }
dependencies?.let { addType(it.spec()) }
objectsImpl?.let { addType(it.spec()) }
Expand Down Expand Up @@ -112,9 +118,18 @@ object KotlinCodeGenerator {
.build()
}

private fun Constructor.spec(): FunSpec =
private fun Constructor.spec(shouldGenerateObserverCode: Boolean, scopeName: String): FunSpec =
FunSpec.constructorBuilder()
.addParameter(dependenciesParameterName, dependenciesClassName.kt)
.apply {
if (shouldGenerateObserverCode) {
addStatement(
"%T.notifyScopeInitializing(%S)",
MotifObserver::class,
scopeName,
)
}
}
.build()

private fun AlternateConstructor.spec(): FunSpec =
Expand Down Expand Up @@ -203,12 +218,37 @@ object KotlinCodeGenerator {
.addStatement("return this")
.build()

private fun FactoryProviderMethod.specs(useNullFieldInitialization: Boolean): List<FunSpec> {
private fun FactoryProviderMethod.specs(
useNullFieldInitialization: Boolean,
shouldGenerateObserverCode: Boolean,
scopeName: String,
): List<FunSpec> {
val primarySpec =
FunSpec.builder(name)
.addModifiers(KModifier.INTERNAL)
.returns(returnTypeName.reloadedForTypeArgs(env))
.addCode(body.spec(useNullFieldInitialization))
.apply {
if (shouldGenerateObserverCode) {
addStatement(
"%T.notifyProvideStart(%S, %S)",
MotifObserver::class,
scopeName,
name,
)
beginControlFlow("try")
addCode(body.spec(useNullFieldInitialization))
nextControlFlow("finally")
addStatement(
"%T.notifyProvideComplete(%S, %S)",
MotifObserver::class,
scopeName,
name,
)
endControlFlow()
} else {
addCode(body.spec(useNullFieldInitialization))
}
}
.build()
val spreadSpecs = spreadProviderMethods.map { it.spec() }
return listOf(primarySpec) + spreadSpecs
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/main/kotlin/motif/compiler/ScopeImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import motif.ast.compiler.CompilerMethod
*/
class ScopeImpl(
val useNullFieldInitialization: Boolean,
val enableObserver: Boolean,
val shouldGenerateObserverCode: Boolean,
val className: ClassName,
val superClassName: ClassName,
val internalScope: Boolean,
Expand Down
19 changes: 15 additions & 4 deletions compiler/src/main/kotlin/motif/compiler/ScopeImplFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,16 @@ private constructor(

fun create(): ScopeImpl {
val isInternal = (scope.clazz as? CompilerClass)?.isInternal() ?: false
val scopeAnnotation =
scope.clazz.annotations.find { it.className == motif.Scope::class.java.name }!!
// Observer code is only generated if both the annotation is enabled AND the environment
// variable is set
val shouldGenerateObserverCode = scope.enableObserver && OBSERVER_ENABLED
return ScopeImpl(
(scope.clazz.annotations
.find { it.className == motif.Scope::class.java.name }!!
.annotationValueMap[SCOPE_ANNOTATION_FIELD_USE_NULL]
as? Boolean) ?: false,
(scopeAnnotation.annotationValueMap[SCOPE_ANNOTATION_FIELD_USE_NULL] as? Boolean)
?: false,
scope.enableObserver,
shouldGenerateObserverCode,
scope.implClassName,
scope.typeName,
isInternal,
Expand Down Expand Up @@ -435,6 +440,12 @@ private constructor(
private const val DEPENDENCIES_FIELD_NAME = "dependencies"
private const val SCOPE_ANNOTATION_FIELD_USE_NULL = "useNullFieldInitialization"

// Check environment variable once for observer code generation
private val OBSERVER_ENABLED: Boolean by lazy {
val envValue = System.getenv("MOTIF_OBSERVE")
envValue == "1" || envValue.equals("True", ignoreCase = true)
}

fun create(env: XProcessingEnv, graph: ResolvedGraph): List<ScopeImpl> =
ScopeImplFactory(env, graph).create()
}
Expand Down
3 changes: 3 additions & 0 deletions lib/src/main/java/motif/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
* [Initialized.INITIALIZED] will be used to skip the field initialization.
*/
boolean useNullFieldInitialization() default false;

/** @return true if this scope should register with MotifObserver and fire lifecycle events. */
boolean enableObserver() default false;
}
Binary file not shown.
110 changes: 110 additions & 0 deletions lib/src/main/java/motif/observe/MotifObserver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2018-2019 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package motif.observe;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
* Central registry for Motif observers. This class manages a list of {@link Observer} instances and
* dispatches lifecycle events to all registered observers.
*
* <p>Events are not cached, observers will only receive events post registration.
*
* <p>This class is thread-safe.
*
* <p>Use -motif.
*/
public final class MotifObserver {

private static final List<Observer> observers = new CopyOnWriteArrayList<>();

private MotifObserver() {
// Prevent instantiation
}

/**
* Registers an observer to receive lifecycle events from Motif scopes.
*
* @param observer the observer to register
*/
public static void register(Observer observer) {
if (observer != null && !observers.contains(observer)) {
observers.add(observer);
}
}

/**
* Unregisters an observer so it no longer receives lifecycle events.
*
* @param observer the observer to unregister
*/
public static void unregister(Observer observer) {
observers.remove(observer);
}

/** Clears all registered observers. */
public static void clearAll() {
observers.clear();
}

/**
* Called by generated ScopeImpl classes when a scope is being initialized.
*
* @param scopeClassName the fully qualified name of the scope being initialized
*/
public static void notifyScopeInitializing(String scopeClassName) {
for (Observer observer : observers) {
try {
observer.onScopeInitializing(scopeClassName);
} catch (Exception e) {
// Silently catch exceptions to prevent observer issues from breaking scope initialization
}
}
}

/**
* Called by generated ScopeImpl classes when a factory/provider method is about to be invoked.
*
* @param scopeClassName the fully qualified name of the scope
* @param methodName the name of the factory/provider method
*/
public static void notifyProvideStart(String scopeClassName, String methodName) {
for (Observer observer : observers) {
try {
observer.onProvideStart(scopeClassName, methodName);
} catch (Exception e) {
// Silently catch exceptions to prevent observer issues from breaking scope functionality
}
}
}

/**
* Called by generated ScopeImpl classes when a factory/provider method has completed execution.
*
* @param scopeClassName the fully qualified name of the scope
* @param methodName the name of the factory/provider method
*/
public static void notifyProvideComplete(String scopeClassName, String methodName) {
for (Observer observer : observers) {
try {
observer.onProvideComplete(scopeClassName, methodName);
} catch (Exception e) {
// Silently catch exceptions to prevent observer issues from breaking scope functionality
}
}
}
}
Binary file added lib/src/main/java/motif/observe/Observer.class
Binary file not shown.
46 changes: 46 additions & 0 deletions lib/src/main/java/motif/observe/Observer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2018-2019 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package motif.observe;

/**
* Observer interface for tracking Scope lifecycle events. Implementations can be registered via
* {@link MotifObserver#register(Observer)}.
*/
public interface Observer {

/**
* Called when a Scope implementation is being initialized.
*
* @param scopeClassName the fully qualified name of the scope being initialized
*/
void onScopeInitializing(String scopeClassName);

/**
* Called when a factory/provider method is about to be invoked.
*
* @param scopeClassName the fully qualified name of the scope
* @param methodName the name of the factory/provider method
*/
void onProvideStart(String scopeClassName, String methodName);

/**
* Called when a factory/provider method has completed execution.
*
* @param scopeClassName the fully qualified name of the scope
* @param methodName the name of the factory/provider method
*/
void onProvideComplete(String scopeClassName, String methodName);
}
Loading
Loading