Remove processor demo and unused tests
Bug: b/248408342
Change-Id: I1b1a5f462f42bbc47d8c5516b9c5e0de8d0696b3
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
deleted file mode 100644
index 1b8d1a6..0000000
--- a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
+++ /dev/null
@@ -1,334 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.processor;
-
-import static org.objectweb.asm.Opcodes.ACC_FINAL;
-import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
-import static org.objectweb.asm.Opcodes.ACC_SUPER;
-
-import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Item;
-import com.android.tools.r8.keepanno.ast.KeepCondition;
-import com.android.tools.r8.keepanno.ast.KeepConsequences;
-import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.ast.KeepEdge.Builder;
-import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
-import com.android.tools.r8.keepanno.ast.KeepItemPattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
-import com.android.tools.r8.keepanno.ast.KeepPreconditions;
-import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
-import com.android.tools.r8.keepanno.ast.KeepTarget;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.function.Consumer;
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Filer;
-import javax.annotation.processing.RoundEnvironment;
-import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
-import javax.lang.model.SourceVersion;
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.AnnotationValue;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ExecutableElement;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.DeclaredType;
-import javax.lang.model.type.TypeMirror;
-import javax.lang.model.util.SimpleAnnotationValueVisitor7;
-import javax.lang.model.util.SimpleTypeVisitor7;
-import javax.tools.Diagnostic.Kind;
-import javax.tools.JavaFileObject;
-import org.objectweb.asm.ClassWriter;
-
-@SupportedAnnotationTypes("com.android.tools.r8.keepanno.annotations.*")
-@SupportedSourceVersion(SourceVersion.RELEASE_7)
-public class KeepEdgeProcessor extends AbstractProcessor {
-
- public static String getClassTypeNameForSynthesizedEdges(String classTypeName) {
- return classTypeName + "$$KeepEdges";
- }
-
- @Override
- @SuppressWarnings("DoNotClaimAnnotations")
- public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
- Map<String, List<KeepEdge>> collectedEdges = new HashMap<>();
- for (TypeElement annotation : annotations) {
- for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
- KeepEdge edge = processKeepEdge(element);
- if (edge != null) {
- TypeElement enclosingType = getEnclosingTypeElement(element);
- String enclosingTypeName = enclosingType.getQualifiedName().toString();
- collectedEdges.computeIfAbsent(enclosingTypeName, k -> new ArrayList<>()).add(edge);
- }
- }
- }
- for (Entry<String, List<KeepEdge>> entry : collectedEdges.entrySet()) {
- String enclosingTypeName = entry.getKey();
- String edgeTargetClass = getClassTypeNameForSynthesizedEdges(enclosingTypeName);
- byte[] writtenEdge = writeEdges(entry.getValue(), edgeTargetClass);
- Filer filer = processingEnv.getFiler();
- try {
- JavaFileObject classFile = filer.createClassFile(edgeTargetClass);
- classFile.openOutputStream().write(writtenEdge);
- } catch (IOException e) {
- error(e.getMessage());
- }
- }
- return true;
- }
-
- private static byte[] writeEdges(List<KeepEdge> edges, String classTypeName) {
- String classBinaryName = AnnotationConstants.getBinaryNameFromClassTypeName(classTypeName);
- ClassWriter classWriter = new ClassWriter(0);
- classWriter.visit(
- KeepEdgeReader.ASM_VERSION,
- ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
- classBinaryName,
- null,
- "java/lang/Object",
- null);
- classWriter.visitSource("SynthesizedKeepEdge", null);
- for (KeepEdge edge : edges) {
- KeepEdgeWriter.writeEdge(edge, classWriter);
- }
- classWriter.visitEnd();
- return classWriter.toByteArray();
- }
-
- @SuppressWarnings("BadImport")
- private KeepEdge processKeepEdge(Element element) {
- AnnotationMirror mirror = getAnnotationMirror(element, AnnotationConstants.Edge.CLASS);
- if (mirror == null) {
- return null;
- }
- Builder edgeBuilder = KeepEdge.builder();
- processPreconditions(edgeBuilder, mirror);
- processConsequences(edgeBuilder, mirror);
- return edgeBuilder.build();
- }
-
- @SuppressWarnings("BadImport")
- private void processPreconditions(Builder edgeBuilder, AnnotationMirror mirror) {
- AnnotationValue preconditions = getAnnotationValue(mirror, Edge.preconditions);
- if (preconditions == null) {
- return;
- }
- KeepPreconditions.Builder preconditionsBuilder = KeepPreconditions.builder();
- new AnnotationListValueVisitor(
- value -> {
- KeepCondition.Builder conditionBuilder = KeepCondition.builder();
- processCondition(conditionBuilder, AnnotationMirrorValueVisitor.getMirror(value));
- preconditionsBuilder.addCondition(conditionBuilder.build());
- })
- .onValue(preconditions);
- edgeBuilder.setPreconditions(preconditionsBuilder.build());
- }
-
- @SuppressWarnings("BadImport")
- private void processConsequences(Builder edgeBuilder, AnnotationMirror mirror) {
- AnnotationValue consequences = getAnnotationValue(mirror, Edge.consequences);
- if (consequences == null) {
- return;
- }
- KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
- new AnnotationListValueVisitor(
- value -> {
- KeepTarget.Builder targetBuilder = KeepTarget.builder();
- processTarget(targetBuilder, AnnotationMirrorValueVisitor.getMirror(value));
- consequencesBuilder.addTarget(targetBuilder.build());
- })
- .onValue(consequences);
- edgeBuilder.setConsequences(consequencesBuilder.build());
- }
-
- private String getTypeNameForClassConstantElement(DeclaredType type) {
- // The processor API does not expose the descriptor or typename, so we need to depend on the
- // sun.tools internals to extract it. If not, this code will not work for inner classes as
- // we cannot recover the $ separator.
- try {
- Object tsym = type.getClass().getField("tsym").get(type);
- Object flatname = tsym.getClass().getField("flatname").get(tsym);
- return flatname.toString();
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new KeepEdgeException("Unable to obtain the class type name for: " + type);
- }
- }
-
- private void processCondition(KeepCondition.Builder builder, AnnotationMirror mirror) {
- KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
- processItem(itemBuilder, mirror);
- builder.setItemPattern(itemBuilder.build());
- }
-
- private void processTarget(KeepTarget.Builder builder, AnnotationMirror mirror) {
- KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
- processItem(itemBuilder, mirror);
- builder.setItemPattern(itemBuilder.build());
- }
-
- private void processItem(KeepItemPattern.Builder builder, AnnotationMirror mirror) {
- AnnotationValue classConstantValue = getAnnotationValue(mirror, Item.classConstant);
- if (classConstantValue != null) {
- DeclaredType type = AnnotationClassValueVisitor.getType(classConstantValue);
- String typeName = getTypeNameForClassConstantElement(type);
- builder.setClassPattern(KeepQualifiedClassNamePattern.exact(typeName));
- }
- AnnotationValue methodNameValue = getAnnotationValue(mirror, Item.methodName);
- AnnotationValue fieldNameValue = getAnnotationValue(mirror, Item.fieldName);
- if (methodNameValue != null && fieldNameValue != null) {
- throw new KeepEdgeException("Cannot define both a method and a field name pattern");
- }
- if (methodNameValue != null) {
- String methodName = AnnotationStringValueVisitor.getString(methodNameValue);
- builder.setMemberPattern(
- KeepMethodPattern.builder()
- .setNamePattern(KeepMethodNamePattern.exact(methodName))
- .build());
- } else if (fieldNameValue != null) {
- String fieldName = AnnotationStringValueVisitor.getString(fieldNameValue);
- builder.setMemberPattern(
- KeepFieldPattern.builder().setNamePattern(KeepFieldNamePattern.exact(fieldName)).build());
- }
- }
-
- private void error(String message) {
- processingEnv.getMessager().printMessage(Kind.ERROR, message);
- }
-
- private static TypeElement getEnclosingTypeElement(Element element) {
- while (true) {
- if (element == null || element instanceof TypeElement) {
- return (TypeElement) element;
- }
- element = element.getEnclosingElement();
- }
- }
-
- private static AnnotationMirror getAnnotationMirror(Element element, Class<?> clazz) {
- String clazzName = clazz.getName();
- for (AnnotationMirror m : element.getAnnotationMirrors()) {
- if (m.getAnnotationType().toString().equals(clazzName)) {
- return m;
- }
- }
- return null;
- }
-
- private static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String key) {
- for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
- annotationMirror.getElementValues().entrySet()) {
- if (entry.getKey().getSimpleName().toString().equals(key)) {
- return entry.getValue();
- }
- }
- return null;
- }
-
- /// Annotation Visitors
-
- private abstract static class AnnotationValueVisitorBase<T>
- extends SimpleAnnotationValueVisitor7<T, Object> {
- @Override
- protected T defaultAction(Object o1, Object o2) {
- throw new IllegalStateException();
- }
-
- public T onValue(AnnotationValue value) {
- return value.accept(this, null);
- }
- }
-
- private static class AnnotationListValueVisitor
- extends AnnotationValueVisitorBase<AnnotationListValueVisitor> {
-
- private final Consumer<AnnotationValue> fn;
-
- public AnnotationListValueVisitor(Consumer<AnnotationValue> fn) {
- this.fn = fn;
- }
-
- @Override
- public AnnotationListValueVisitor visitArray(
- List<? extends AnnotationValue> values, Object ignore) {
- values.forEach(fn);
- return this;
- }
- }
-
- private static class AnnotationMirrorValueVisitor
- extends AnnotationValueVisitorBase<AnnotationMirrorValueVisitor> {
-
- private AnnotationMirror mirror = null;
-
- public static AnnotationMirror getMirror(AnnotationValue value) {
- return new AnnotationMirrorValueVisitor().onValue(value).mirror;
- }
-
- @Override
- public AnnotationMirrorValueVisitor visitAnnotation(AnnotationMirror mirror, Object o) {
- this.mirror = mirror;
- return this;
- }
- }
-
- private static class AnnotationStringValueVisitor
- extends AnnotationValueVisitorBase<AnnotationStringValueVisitor> {
- private String string;
-
- public static String getString(AnnotationValue value) {
- return new AnnotationStringValueVisitor().onValue(value).string;
- }
-
- @Override
- public AnnotationStringValueVisitor visitString(String string, Object ignore) {
- this.string = string;
- return this;
- }
- }
-
- private static class AnnotationClassValueVisitor
- extends AnnotationValueVisitorBase<AnnotationClassValueVisitor> {
- private DeclaredType type = null;
-
- public static DeclaredType getType(AnnotationValue value) {
- return new AnnotationClassValueVisitor().onValue(value).type;
- }
-
- @Override
- public AnnotationClassValueVisitor visitType(TypeMirror t, Object ignore) {
- ClassTypeVisitor classTypeVisitor = new ClassTypeVisitor();
- t.accept(classTypeVisitor, null);
- type = classTypeVisitor.type;
- return this;
- }
- }
-
- private static class TypeVisitorBase<T> extends SimpleTypeVisitor7<T, Object> {
- @Override
- protected T defaultAction(TypeMirror typeMirror, Object ignore) {
- throw new IllegalStateException();
- }
- }
-
- private static class ClassTypeVisitor extends TypeVisitorBase<ClassTypeVisitor> {
- private DeclaredType type = null;
-
- @Override
- public ClassTypeVisitor visitDeclared(DeclaredType t, Object ignore) {
- this.type = t;
- return this;
- }
- }
-}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
deleted file mode 100644
index fd8154f..0000000
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
+++ /dev/null
@@ -1,255 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno;
-
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeTrue;
-
-import com.android.tools.r8.JavaCompilerTool;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
-import com.android.tools.r8.keepanno.asm.KeepEdgeWriter.AnnotationVisitorInterface;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants;
-import com.android.tools.r8.keepanno.ast.AnnotationConstants.Edge;
-import com.android.tools.r8.keepanno.ast.KeepDeclaration;
-import com.android.tools.r8.keepanno.ast.KeepEdge;
-import com.android.tools.r8.keepanno.processor.KeepEdgeProcessor;
-import com.android.tools.r8.keepanno.testsource.KeepClassAndDefaultConstructorSource;
-import com.android.tools.r8.keepanno.testsource.KeepDependentFieldSource;
-import com.android.tools.r8.keepanno.testsource.KeepFieldSource;
-import com.android.tools.r8.keepanno.testsource.KeepSourceEdges;
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.transformers.ClassTransformer;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.ZipUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.objectweb.asm.AnnotationVisitor;
-
-@Ignore("b/248408342: These test break on r8lib builds because of src&test using ASM classes.")
-@RunWith(Parameterized.class)
-public class KeepEdgeAnnotationsTest extends TestBase {
-
- private static class ParamWrapper {
- private final Class<?> clazz;
- private final TestParameters params;
-
- public ParamWrapper(Class<?> clazz, TestParameters params) {
- this.clazz = clazz;
- this.params = params;
- }
-
- @Override
- public String toString() {
- return clazz.getSimpleName() + ", " + params.toString();
- }
- }
-
- private static List<Class<?>> getTestClasses() {
- return ImmutableList.of(
- KeepClassAndDefaultConstructorSource.class,
- KeepFieldSource.class,
- KeepDependentFieldSource.class);
- }
-
- private final TestParameters parameters;
- private final Class<?> source;
-
- @Parameterized.Parameters(name = "{0}")
- public static List<ParamWrapper> data() {
- TestParametersCollection params =
- getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
- return getTestClasses().stream()
- .flatMap(c -> params.stream().map(p -> new ParamWrapper(c, p)))
- .collect(Collectors.toList());
- }
-
- public KeepEdgeAnnotationsTest(ParamWrapper wrapper) {
- this.parameters = wrapper.params;
- this.source = wrapper.clazz;
- }
-
- private String getExpected() {
- return KeepSourceEdges.getExpected(source);
- }
-
- @Test
- public void testProcessorClassfiles() throws Exception {
- assumeTrue(parameters.isCfRuntime());
- Path out =
- JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
- .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
- .addClasspathFiles(ToolHelper.getKeepAnnoPath())
- .addClassNames(Collections.singletonList(typeName(source)))
- .addClasspathFiles(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "test"))
- .addClasspathFiles(ToolHelper.getR8WithRelocatedDeps())
- .compile();
-
- CodeInspector inspector = new CodeInspector(out);
- checkSynthesizedKeepEdgeClass(inspector, out);
- // The source is added as a classpath name but not part of the compilation unit output.
- assertThat(inspector.clazz(source), isAbsent());
-
- testForJvm(parameters)
- .addProgramClassesAndInnerClasses(source)
- .addProgramFiles(out)
- .run(parameters.getRuntime(), source)
- .assertSuccessWithOutput(getExpected());
- }
-
- @Test
- public void testProcessorJavaSource() throws Exception {
- assumeTrue(parameters.isCfRuntime());
- Path out =
- JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
- .addSourceFiles(ToolHelper.getSourceFileForTestClass(source))
- .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
- .addClasspathFiles(ToolHelper.getKeepAnnoPath())
- .addClasspathFiles(ToolHelper.getDeps())
- .compile();
- testForJvm(parameters)
- .addProgramFiles(out)
- .run(parameters.getRuntime(), source)
- .assertSuccessWithOutput(getExpected())
- .inspect(
- inspector -> {
- assertThat(inspector.clazz(source), isPresent());
- checkSynthesizedKeepEdgeClass(inspector, out);
- });
- }
-
- public static List<byte[]> getInputClassesWithoutKeepAnnotations(Collection<Class<?>> classes)
- throws Exception {
- List<byte[]> transformed = new ArrayList<>(classes.size());
- for (Class<?> clazz : classes) {
- transformed.add(
- transformer(clazz).removeAnnotations(AnnotationConstants::isKeepAnnotation).transform());
- }
- return transformed;
- }
-
- /** Wrapper to bridge ASM visitors when using the r8lib compiled version of the keepanno lib. */
- private AnnotationVisitorInterface wrap(AnnotationVisitor visitor) {
- if (visitor == null) {
- return null;
- }
- return new AnnotationVisitorInterface() {
- @Override
- public int version() {
- return KeepEdgeReader.ASM_VERSION;
- }
-
- @Override
- public void visit(String name, Object value) {
- visitor.visit(name, value);
- }
-
- @Override
- public void visitEnum(String name, String descriptor, String value) {
- visitor.visitEnum(name, descriptor, value);
- }
-
- @Override
- public AnnotationVisitorInterface visitAnnotation(String name, String descriptor) {
- AnnotationVisitor v = visitor.visitAnnotation(name, descriptor);
- return v == visitor ? this : wrap(v);
- }
-
- @Override
- public AnnotationVisitorInterface visitArray(String name) {
- AnnotationVisitor v = visitor.visitArray(name);
- return v == visitor ? this : wrap(v);
- }
-
- @Override
- public void visitEnd() {
- visitor.visitEnd();
- }
- };
- }
-
- @Test
- public void testAsmReader() throws Exception {
- assumeTrue(parameters.isCfRuntime());
- List<KeepEdge> expectedEdges = KeepSourceEdges.getExpectedEdges(source);
- ClassReference clazz = Reference.classFromClass(source);
- // Original bytes of the test class.
- byte[] original = ToolHelper.getClassAsBytes(source);
- // Strip out all the annotations to ensure they are actually added again.
- byte[] stripped =
- getInputClassesWithoutKeepAnnotations(Collections.singletonList(source)).get(0);
- // Manually add in the expected edges again.
- byte[] readded =
- transformer(stripped, clazz)
- .addClassTransformer(
- new ClassTransformer() {
-
- @Override
- public void visitEnd() {
- for (KeepEdge edge : expectedEdges) {
- KeepEdgeWriter.writeEdge(
- edge, (desc, visible) -> wrap(super.visitAnnotation(desc, visible)));
- }
- super.visitEnd();
- }
- })
- .transform();
-
- // Read the edges from each version.
- List<KeepDeclaration> originalEdges = KeepEdgeReader.readKeepEdges(original);
- List<KeepDeclaration> strippedEdges = KeepEdgeReader.readKeepEdges(stripped);
- List<KeepDeclaration> readdedEdges = KeepEdgeReader.readKeepEdges(readded);
-
- // The edges are compared to the "expected" ast to ensure we don't hide failures in reading or
- // writing.
- assertEquals(Collections.emptyList(), strippedEdges);
- assertEquals(expectedEdges, originalEdges);
- assertEquals(expectedEdges, readdedEdges);
- }
-
- @Test
- public void testExtractAndRun() throws Exception {
- testForR8(parameters.getBackend())
- .enableExperimentalKeepAnnotations()
- .addProgramClassesAndInnerClasses(source)
- .addKeepMainRule(source)
- .setMinApi(parameters)
- .run(parameters.getRuntime(), source)
- .assertSuccessWithOutput(getExpected());
- }
-
- private void checkSynthesizedKeepEdgeClass(CodeInspector inspector, Path data)
- throws IOException {
- String synthesizedEdgesClassName =
- KeepEdgeProcessor.getClassTypeNameForSynthesizedEdges(source.getTypeName());
- ClassSubject synthesizedEdgesClass = inspector.clazz(synthesizedEdgesClassName);
- assertThat(synthesizedEdgesClass, isPresent());
- assertThat(synthesizedEdgesClass.annotation(Edge.CLASS.getTypeName()), isPresent());
- String entry = ZipUtils.zipEntryNameForClass(synthesizedEdgesClass.getFinalReference());
- byte[] bytes = ZipUtils.readSingleEntry(data, entry);
- List<KeepDeclaration> keepEdges = KeepEdgeReader.readKeepEdges(bytes);
- assertEquals(KeepSourceEdges.getExpectedEdges(source), keepEdges);
- }
-}