Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ public MappingData getMapping(Types types, TypeMirror type) {

@Nullable
public MappingData getMapping(ClassName type) {
if (this.mapperClasses == null) {
if (this.mapperClasses == null && this.tag == null) {
return null;
}
for (var mapperClass : mapperClasses) {
for (var mapperClass : Objects.requireNonNullElse(mapperClasses, List.<TypeMirror>of())) {
if (doesImplement(mapperClass, type)) {
return new MappingData(mapperClass, this.tag);
}
Expand Down
2 changes: 2 additions & 0 deletions logging/logging-annotation-processor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ dependencies {
implementation project(':aop:aop-annotation-processor')

testImplementation project(':logging:logging-common')
testImplementation project(':json:json-annotation-processor')
testImplementation project(':core:kora-app-annotation-processor')
testImplementation testFixtures(project(":core:annotation-processor-common"))
testImplementation project(':logging:logging-logback')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.koraframework.logging.annotation.processor;

import com.palantir.javapoet.ClassName;
import io.koraframework.annotation.processor.common.AbstractKoraProcessor;
import io.koraframework.annotation.processor.common.AnnotationUtils;
import io.koraframework.annotation.processor.common.ProcessingErrorException;
import io.koraframework.logging.annotation.processor.aop.LogAspectClassNames;

import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.List;
import java.util.Map;
import java.util.Set;

public final class LoggingAnnotationProcessor extends AbstractKoraProcessor {

private MaskingRulesProcessor maskingRulesProcessor;

@Override
public Set<ClassName> getSupportedAnnotationClassNames() {
return Set.of(LogAspectClassNames.mask);
}

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.maskingRulesProcessor = new MaskingRulesProcessor(processingEnv);
}

@Override
protected void process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv, Map<ClassName, List<AnnotatedElement>> annotatedElements) {
var maskedElements = annotatedElements.getOrDefault(LogAspectClassNames.mask, List.of());
for (var annotated : maskedElements) {
var element = annotated.element();
if (!(element instanceof TypeElement typeElement)) {
continue;
}
if (!typeElement.getKind().isClass()) {
this.messager.printMessage(Diagnostic.Kind.ERROR, "Only classes and records can be annotated with @Mask", element);
continue;
}
if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
this.messager.printMessage(Diagnostic.Kind.ERROR, "Abstract classes can't be annotated with @Mask", element);
continue;
}
if (AnnotationUtils.findAnnotation(typeElement, LogAspectClassNames.mask) == null) {
continue;
}
try {
this.maskingRulesProcessor.generate(typeElement);
} catch (ProcessingErrorException e) {
e.printError(this.processingEnv);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package io.koraframework.logging.annotation.processor;

import com.palantir.javapoet.*;
import io.koraframework.annotation.processor.common.*;
import io.koraframework.logging.annotation.processor.aop.LogAspectClassNames;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.util.*;

public final class MaskingRulesProcessor {

private final ProcessingEnvironment env;
private final Elements elements;
private final Types types;

public MaskingRulesProcessor(ProcessingEnvironment env) {
this.env = env;
this.elements = env.getElementUtils();
this.types = env.getTypeUtils();
}

public void generate(TypeElement root) {
var packageName = this.elements.getPackageOf(root).getQualifiedName().toString();
var className = metadataName(root);
if (this.elements.getTypeElement(packageName + "." + className) != null) {
return;
}

var rules = new ArrayList<MaskingRuleMeta>();
this.visit(root, new ArrayList<>(), new HashSet<>(), rules);
var strategies = this.strategies(rules);

var rootType = TypeName.get(root.asType());
var rulesType = ParameterizedTypeName.get(LogAspectClassNames.maskingRules, rootType);
var typeBuilder = TypeSpec.interfaceBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(CommonClassNames.module)
.addAnnotation(AnnotationUtils.generated(MaskingRulesProcessor.class))
.addMethod(this.factoryMethod(root, rulesType, rules, strategies));

CommonUtils.safeWriteTo(this.env, JavaFile.builder(packageName, typeBuilder.build()).build());
}

private MethodSpec factoryMethod(TypeElement root, TypeName rulesType, List<MaskingRuleMeta> rules, Map<String, StrategyMeta> strategies) {
var method = MethodSpec.methodBuilder(CommonUtils.decapitalize(root.getSimpleName().toString()) + "MaskingRules")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addAnnotation(CommonClassNames.defaultComponent)
.returns(rulesType);
for (var strategy : strategies.values()) {
method.addParameter(TypeName.get(strategy.type()), strategy.fieldName());
}
method.addStatement("return $L", this.rulesCode(root, rules, strategies));
return method.build();
}

private CodeBlock rulesCode(TypeElement root, List<MaskingRuleMeta> rules, Map<String, StrategyMeta> strategies) {
var code = CodeBlock.builder()
.add("$T.builder($T.class)", LogAspectClassNames.maskingRules, TypeName.get(root.asType()));
for (var rule : rules) {
var strategy = strategies.get(rule.strategy().toString());
code.add("\n.mask($S, $N)", String.join(".", rule.path()), strategy.fieldName());
}
return code.add("\n.build()").build();
}

private Map<String, StrategyMeta> strategies(List<MaskingRuleMeta> rules) {
var strategies = new LinkedHashMap<String, StrategyMeta>();
for (var rule : rules) {
strategies.computeIfAbsent(rule.strategy().toString(), k -> new StrategyMeta(rule.strategy(), "strategy" + strategies.size()));
}
return strategies;
}

private void visit(TypeElement type, List<String> path, Set<String> branch, List<MaskingRuleMeta> rules) {
if (!this.isJsonOrMasked(type)) {
return;
}
var key = type.getQualifiedName().toString();
if (!branch.add(key)) {
return;
}

var typeMask = findMask(type);
for (var field : this.parse(type)) {
var fieldPath = new ArrayList<>(path);
fieldPath.add(field.jsonName());
if (field.mask() != null) {
rules.add(new MaskingRuleMeta(fieldPath, this.maskStrategy(field.mask(), typeMask), false));
continue;
}
this.visitFieldType(field.type(), fieldPath, branch, rules);
}
branch.remove(key);
}

private void visitFieldType(TypeMirror type, List<String> path, Set<String> branch, List<MaskingRuleMeta> rules) {
if (type.getKind() == TypeKind.ARRAY && type instanceof ArrayType arrayType) {
this.visitFieldType(arrayType.getComponentType(), path, branch, rules);
return;
}
if (type.getKind() != TypeKind.DECLARED || !(type instanceof DeclaredType declaredType)) {
return;
}
if (CommonUtils.isCollection(type)) {
if (!declaredType.getTypeArguments().isEmpty()) {
this.visitFieldType(declaredType.getTypeArguments().get(0), path, branch, rules);
}
return;
}
if (CommonUtils.isMap(type)) {
if (declaredType.getTypeArguments().size() >= 2) {
var valuePath = new ArrayList<>(path);
valuePath.add("*");
this.visitFieldType(declaredType.getTypeArguments().get(1), valuePath, branch, rules);
}
return;
}

var element = this.types.asElement(type);
if (element instanceof TypeElement typeElement) {
this.visit(typeElement, path, branch, rules);
}
}

private TypeMirror maskStrategy(AnnotationMirror mask, AnnotationMirror typeMask) {
var strategyType = AnnotationUtils.<TypeMirror>parseAnnotationValueWithoutDefault(mask, "value");
if (strategyType != null) {
return strategyType;
}
if (typeMask != null) {
return AnnotationUtils.parseAnnotationValue(this.elements, typeMask, "value");
}
return this.elements.getTypeElement(LogAspectClassNames.maskingFull.canonicalName()).asType();
}

private List<MaskingField> parse(TypeElement type) {
var fields = new ArrayList<MaskingField>();
var nameConverter = CommonUtils.getNameConverter(type);
if (type.getKind() == ElementKind.RECORD) {
for (var enclosed : type.getEnclosedElements()) {
if (!(enclosed instanceof RecordComponentElement component)) {
continue;
}
if (AnnotationUtils.findAnnotation(component, LogAspectClassNames.jsonSkip) != null) {
continue;
}
var accessor = this.findRecordAccessor(type, component);
var jsonName = this.jsonName(component, nameConverter);
var mask = findMask(component, accessor);
fields.add(new MaskingField(jsonName, component.asType(), mask));
}
} else {
for (var enclosed : type.getEnclosedElements()) {
if (enclosed.getKind() != ElementKind.FIELD || !(enclosed instanceof VariableElement field)) {
continue;
}
if (field.getModifiers().contains(Modifier.STATIC)) {
continue;
}
if (AnnotationUtils.findAnnotation(field, LogAspectClassNames.jsonSkip) != null) {
continue;
}
var jsonName = this.jsonName(field, nameConverter);
var accessor = this.findAccessor(type, field);
var mask = accessor == null ? findMask(field) : findMask(field, accessor);
fields.add(new MaskingField(jsonName, field.asType(), mask));
}
}
return fields;
}

private String jsonName(Element field, CommonUtils.NameConverter nameConverter) {
var jsonField = AnnotationUtils.findAnnotation(field, LogAspectClassNames.jsonField);
if (jsonField != null) {
var value = AnnotationUtils.<String>parseAnnotationValueWithoutDefault(jsonField, "value");
if (value != null && !value.isBlank()) {
return value;
}
}
var name = field.getSimpleName().toString();
return nameConverter == null ? name : nameConverter.convert(name);
}

private ExecutableElement findRecordAccessor(TypeElement type, RecordComponentElement component) {
return CommonUtils.findMethods(type, m -> m.contains(Modifier.PUBLIC))
.stream()
.filter(m -> m.getParameters().isEmpty())
.filter(m -> m.getSimpleName().contentEquals(component.getSimpleName()))
.findFirst()
.orElse(null);
}

private ExecutableElement findAccessor(TypeElement type, VariableElement field) {
var name = field.getSimpleName().toString();
var capitalized = CommonUtils.capitalize(name);
return CommonUtils.findMethods(type, m -> m.contains(Modifier.PUBLIC))
.stream()
.filter(m -> m.getParameters().isEmpty())
.filter(m -> m.getSimpleName().contentEquals(name) || m.getSimpleName().contentEquals("get" + capitalized))
.findFirst()
.orElse(null);
}

private boolean isJsonOrMasked(TypeElement typeElement) {
return AnnotationUtils.findAnnotation(typeElement, LogAspectClassNames.json) != null
|| AnnotationUtils.findAnnotation(typeElement, LogAspectClassNames.jsonWriter) != null
|| AnnotationUtils.findAnnotation(typeElement, LogAspectClassNames.mask) != null;
}

private static AnnotationMirror findMask(Element... elements) {
for (var element : elements) {
var mask = AnnotationUtils.findAnnotation(element, LogAspectClassNames.mask);
if (mask != null) {
return mask;
}
}
return null;
}

public static String metadataName(TypeElement type) {
return NameUtils.generatedType(type, "MaskingRulesModule");
}

private record MaskingField(String jsonName, TypeMirror type, AnnotationMirror mask) {}

private record MaskingRuleMeta(List<String> path, TypeMirror strategy, boolean fieldOnly) {}

private record StrategyMeta(TypeMirror type, String fieldName) {}
}
Loading
Loading