/*
* Copyright (C) 2015 Square, 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 com.squareup.auto.value.redacted;
import com.google.auto.service.AutoService;
import com.google.auto.value.extension.AutoValueExtension;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
@AutoService(AutoValueExtension.class)
public final class AutoValueRedactedExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
Map<String, ExecutableElement> properties = context.properties();
for (ExecutableElement element : properties.values()) {
if (getAnnotations(element).contains("Redacted")) {
return true;
}
}
return false;
}
@Override
public String generateClass(Context context, String className, String classToExtend,
boolean isFinal) {
String packageName = context.packageName();
Name superName = context.autoValueClass().getSimpleName();
Map<String, ExecutableElement> properties = context.properties();
TypeSpec subclass = TypeSpec.classBuilder(className) //
.addModifiers(isFinal ? Modifier.FINAL : Modifier.ABSTRACT) //
.superclass(ClassName.get(packageName, classToExtend)) //
.addMethod(generateConstructor(properties)) //
.addMethod(generateToString(superName, properties)) //
.build();
JavaFile javaFile = JavaFile.builder(packageName, subclass).build();
return javaFile.toString();
}
private static MethodSpec generateConstructor(Map<String, ExecutableElement> properties) {
List<ParameterSpec> params = new ArrayList<>();
for (Map.Entry<String, ExecutableElement> entry : properties.entrySet()) {
TypeName typeName = TypeName.get(entry.getValue().getReturnType());
params.add(ParameterSpec.builder(typeName, entry.getKey()).build());
}
StringBuilder body = new StringBuilder("super(");
for (int i = properties.size(); i > 0; i--) {
body.append("$N");
if (i > 1) body.append(", ");
}
body.append(")");
return MethodSpec.constructorBuilder() //
.addParameters(params) //
.addStatement(body.toString(), properties.keySet().toArray()) //
.build();
}
private static MethodSpec generateToString(Name superName,
Map<String, ExecutableElement> properties) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("toString") //
.addAnnotation(Override.class) //
.addModifiers(Modifier.PUBLIC, Modifier.FINAL) //
.returns(String.class) //
.addCode("return \"$L{\"\n", superName) //
.addCode("$>$>");
int count = 0;
for (Map.Entry<String, ExecutableElement> entry : properties.entrySet()) {
String propertyName = entry.getKey();
ExecutableElement propertyElement = entry.getValue();
String methodName = propertyElement.getSimpleName().toString();
TypeName propertyType = TypeName.get(entry.getValue().getReturnType());
Set<String> propertyAnnotations = getAnnotations(propertyElement);
boolean redacted = propertyAnnotations.contains("Redacted");
boolean nullable = propertyAnnotations.contains("Nullable");
boolean last = ++count == properties.size();
// Special-case this configuration since we can pre-concat constant strings.
if (redacted && !nullable) {
builder.addCode("+ \"$N=██", propertyName);
if (!last) builder.addCode(", ");
builder.addCode("\"\n");
continue;
}
builder.addCode("+ \"$N=\" + ", propertyName);
CodeBlock propertyToString;
if (redacted) {
propertyToString = CodeBlock.of("\"██\"");
} else if (propertyType instanceof ArrayTypeName) {
propertyToString = CodeBlock.of("$T.toString($N())", Arrays.class, methodName);
} else {
propertyToString = CodeBlock.of("$N()", methodName);
}
if (nullable) {
builder.addCode("($N() != null ? $L : null)", methodName, propertyToString);
} else {
builder.addCode(propertyToString);
}
if (!last) {
builder.addCode(" + \", \"");
}
builder.addCode("\n");
}
return builder //
.addStatement("+ '}'") //
.addCode("$<$<")
.build();
}
private static Set<String> getAnnotations(ExecutableElement element) {
Set<String> set = new LinkedHashSet<>();
List<? extends AnnotationMirror> annotations = element.getAnnotationMirrors();
for (AnnotationMirror annotation : annotations) {
set.add(annotation.getAnnotationType().asElement().getSimpleName().toString());
}
return Collections.unmodifiableSet(set);
}
}