Java中的屠龙之术
Java中的屠龙之术
JCTree的介绍
CTree(Java Compiler Tree API)是 Java 编译器提供的一组 API,用于操作和分析 Java 代码的抽象语法树(AST)。JCTree API 在
com.sun.tools.javac.tree包中提供,并且是 Java 编译器的一部分。它允许开发者在编译期间访问和修改 Java 代码的结构,从而实现代码生成、代码检查、代码转换等功能。JCTree API 允许你以编程方式操作 AST 中的不同元素,如类、方法、字段、表达式等
以下是一些常见的 JCTree API 类和接口,以及它们的一些重要方法:
- JCCompilationUnit:表示整个编译单元(源文件)的抽象语法树。它是 AST 的根节点。
- JCClassDecl:表示类或接口的声明。
- JCMethodDecl:表示方法的声明。
- JCVariableDecl:表示变量(字段)的声明。
- JCExpression:表示表达式,如赋值、算术运算、方法调用等。
- JCAssign:赋值语句语法树节点
- JCIdent:标识符语法树节点,可以是变量,类型,关键字等等
- JCStatement:表示语句,如 if、while、for、return 等。
- JCBlock:语句块语法树节点
- JCReturn:return语句语法树节点
- JCTreeVisitor
:用于遍历和访问 AST 的访问者接口。你可以通过实现这个接口来定义如何处理不同类型的节点。
JavacTrees
com.sun.tools.javac.api.JavacTrees是 Java 编译器(javac)提供的一个工具类,用于在注解处理器中访问和操作抽象语法树(AST)。它为注解处理器提供了一些便捷的方法,使得开发者可以更容易地在编译时处理 Java 源代码的结构。
JavacTrees类主要用于获取与 AST 相关的一些工具和对象,包括:
TreeMaker:用于创建新的 AST 节点,如创建类、方法、字段、表达式等。
Element和TypeElement的转换:可以将Element对象(表示程序元素)转换为对应的TypeElement、ExecutableElement等对象,以便进行更详细的操作。
getTree(Element element):获取给定程序元素的对应 AST 节点。通过调用这个方法,你可以获取到表示该元素的 AST 节点。
getScope(Element element):获取给定程序元素的对应作用域对象。这个作用域对象可以用于查询和分析元素的可见性等信息。请注意,
com.sun.tools.javac.api.JavacTrees类属于com.sun.tools.javac包,这意味着它位于javac编译器的内部实现中。以下是一个简单的示例,演示了如何在注解处理器中使用
JavacTrees获取 AST 相关的信息:
TreeMaker
TreeMaker.Modifiers
TreeMaker.Modifiers方法用于创建访问标志语法树节点(JCModifiers)public JCModifiers Modifiers(long flags) {return Modifiers(flags, List.< JCAnnotation >nil()); }public JCModifiers Modifiers(long flags,Listannotations) {JCModifiers tree = new JCModifiers(flags, annotations);boolean noFlags = (flags & (Flags.ModifierFlags | Flags.ANNOTATION)) == 0;tree.pos = (noFlags && annotations.isEmpty()) ? Position.NOPOS : pos;return tree; }
- flags:访问标志
- annotations:注解列表
treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL);public static final
TreeMaker.ClassDef
TreeMaker.ClassDef用于创建类定义语法树节点(JCClassDecl)
public JCClassDecl ClassDef(JCModifiers mods,Name name,Listtyparams,JCExpression extending,List implementing,List defs) {JCClassDecl tree = new JCClassDecl(mods,name,typarams,extending,implementing,defs,null);tree.pos = pos;return tree; }
- mods:访问标志,可以通过
TreeMaker.Modifiers来创建- name:类名
- typarams:泛型参数列表
- extending:父类
- implementing:实现的接口
- defs:类定义的详细语句,包括字段、方法的定义等等
TreeMaker.MethodDef
TreeMaker.MethodDef用于创建方法定义语法树节点(JCMethodDecl)
public JCMethodDecl MethodDef(JCModifiers mods,Name name,JCExpression restype,Listtyparams,List params,List thrown,JCBlock body,JCExpression defaultValue) {JCMethodDecl tree = new JCMethodDecl(mods,name,restype,typarams,params,thrown,body,defaultValue,null);tree.pos = pos;return tree; }public JCMethodDecl MethodDef(MethodSymbol m,Type mtype,JCBlock body) {return (JCMethodDecl)new JCMethodDecl(Modifiers(m.flags(), Annotations(m.getAnnotationMirrors())),m.name,Type(mtype.getReturnType()),TypeParams(mtype.getTypeArguments()),Params(mtype.getParameterTypes(), m),Types(mtype.getThrownTypes()),body,null,m).setPos(pos).setType(mtype); }
- mods:访问标志
- name:方法名
- restype:返回类型
- typarams:泛型参数列表
- params:参数列表
- thrown:异常声明列表
- body:方法体
- defaultValue:默认方法(可能是interface中的哪个default)
- m:方法符号
- mtype:方法类型。包含多种类型,泛型参数类型、方法参数类型、异常参数类型、返回参数类型。
TreeMaker.VarDef
TreeMaker.VarDef用于创建字段/变量定义语法树节点
public JCVariableDecl VarDef(JCModifiers mods,Name name,JCExpression vartype,JCExpression init) {JCVariableDecl tree = new JCVariableDecl(mods, name, vartype, init, null);tree.pos = pos;return tree; }public JCVariableDecl VarDef(VarSymbol v,JCExpression init) {return (JCVariableDecl)new JCVariableDecl(Modifiers(v.flags(), Annotations(v.getAnnotationMirrors())),v.name,Type(v.type),init,v).setPos(pos).setType(v.type); }
- mods:访问标志
- name:参数名称
- vartype:类型
- init:初始化语句
- v:变量符号
TreeMaker.Ident
TreeMaker.Ident用于创建标识符语法树节点
public static class JCIdent extends JCTree.JCExpression implements IdentifierTree {public Name name;//标识符的名字public Symbol sym;//代表类时为包名+类名,代表其他类型数据时为nullprotected JCIdent(Name var1, Symbol var2) {this.name = var1;this.sym = var2;} }-->创建实例: 获取变量textView的引用 treeMaker.Ident(names.fromString("textView"))))
TreeMaker.Return
TreeMaker.Return用于创建return语句
public static class JCReturn extends JCTree.JCStatement implements ReturnTree {public JCTree.JCExpression expr;//返回语句的结果字段 }-->例子:retrun this.xxx treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")),names.fromString("xxx")));
TreeMaker.Select
TreeMaker.Select用于创建域访问/方法访问
public JCFieldAccess Select(JCExpression selected,Name selector) {JCFieldAccess tree = new JCFieldAccess(selected, selector, null);tree.pos = pos;return tree; }public JCExpression Select(JCExpression base,Symbol sym) {return new JCFieldAccess(base, sym.name, sym).setPos(pos).setType(sym.type); }
- selected:
.运算符左边的表达式- selector:
.运算符右边的表达式TreeMaker.Select(treeMaker.Ident(names.fromString("this")), names.fromString("name"));this.name
TreeMaker.Apply
TreeMaker.Apply用于创建方法调用语法树节点
public JCMethodInvocation Apply(Listtypeargs,JCExpression fn,List args) {JCMethodInvocation tree = new JCMethodInvocation(typeargs, fn, args);tree.pos = pos;return tree; }
- typeargs:参数类型列表
- fn:调用语句
- args:参数列表
TreeMaker.Assign
TreeMaker.Assign用户创建赋值语句语法树节点
ublic JCAssign Assign(JCExpression lhs,JCExpression rhs) {JCAssign tree = new JCAssign(lhs, rhs);tree.pos = pos;return tree; }
- lhs:赋值语句左边表达式
- rhs:赋值语句右边表达式
TreeMaker.Exec
TreeMaker.Exec用于创建可执行语句语法树节点
public JCExpressionStatement Exec(JCExpression expr) {JCExpressionStatement tree = new JCExpressionStatement(expr);tree.pos = pos;return tree; }TreeMaker.Apply以及TreeMaker.Assign就需要外面包一层TreeMaker.Exec来获得一个JCExpressionStatement
TreeMaker.Block
TreeMaker.Block用于创建组合语句的语法树节点
public JCBlock Block(long flags,Liststats) {JCBlock tree = new JCBlock(flags, stats);tree.pos = pos;return tree; }
- flags:访问标志
- stats:语句列表
public static class JCBlock extends JCTree.JCStatement implements BlockTree {public long flags;//访问修复符public Liststats;//多行代码列表 }-->使用例子: List jcStatementList = List.nil(); treeMaker.Block(0, jcStatementList);//构建代码块
JCIf
if代码块; if(condition) {thenpart} else {elsepart}
public static class JCIf extends JCTree.JCStatement implements IfTree {public JCTree.JCExpression cond;//条件语句public JCTree.JCStatement thenpart;//if的操作语句public JCTree.JCStatement elsepart;//else的操作语句 }
JCForLoop
for循环代码块;for (init; cond; step) {body}
public static class JCForLoop extends JCTree.JCStatement implements ForLoopTree {public Listinit;public JCTree.JCExpression cond;public List step;public JCTree.JCStatement body; }
JCTry、JCCatch
public static class JCTry extends JCTree.JCStatement implements TryTree {public JCTree.JCBlock body;//try代码块public List<JCTree.JCCatch> catchers;//JCCatchpublic JCTree.JCBlock finalizer;//final代码块public List<JCTree> resources;//List.nil(),用不上的字段public boolean finallyCanCompleteNormally;// }-->JCCatch public static class JCCatch extends JCTree implements CatchTree {public JCTree.JCVariableDecl param;//catch的异常类型public JCTree.JCBlock body;//catch代码块 }
AbstractProcessor
AbstractProcessor是 Java 注解处理器的一个抽象类,它是 Java 编译器提供的工具,用于处理源代码中的注解。注解处理器可以在编译时扫描和处理源代码中的注解信息,并根据注解生成代码、进行静态分析、执行代码检查等操作。注解处理器是 Java 编译器的一部分,它可以用于生成额外的代码,修改现有的代码结构,或者在编译期间执行其他任务。这使得注解处理器非常适用于一些元编程和自动化的场景。
AbstractProcessor作为注解处理器的抽象类,提供了一些常用的方法和操作,使得开发者可以更方便地编写自定义的注解处理器。它是 Java 标准库中的一部分,位于javax.annotation.processing包中。以下是
AbstractProcessor类的一些重要方法和用途:
init方法:在注解处理器被初始化时调用。开发者可以在这里进行初始化操作。
getSupportedAnnotationTypes方法:返回一个字符串集合,表示该处理器支持处理哪些注解。这些注解可以是完全限定名,也可以使用通配符。
getSupportedSourceVersion方法:返回一个表示支持的 Java 源代码版本的枚举值。通常返回SourceVersion.latestSupported()。
process方法:核心方法,用于处理注解。在这个方法中,你可以获取到编译器提供的注解信息和相关元素,然后执行处理逻辑。
ProcessingEnvironment
javax.annotation.processing.ProcessingEnvironment是 Java 注解处理器的一个核心接口,它提供了注解处理器在编译时与编译环境进行交互的能力。通过ProcessingEnvironment,注解处理器可以访问编译器提供的各种工具和信息,从而实现自定义的代码生成、静态分析、代码检查等功能。
ProcessingEnvironment接口定义了许多有用的方法,使得注解处理器能够获取有关编译环境的各种信息。以下是一些ProcessingEnvironment接口中常用的方法:
Messager getMessager():获取用于输出消息的Messager对象,允许注解处理器在编译时输出信息、警告和错误。
Filer getFiler():获取用于生成文件的Filer对象,允许注解处理器生成新的源代码文件、类文件等。
Elements getElementUtils():获取用于访问程序元素的Elements对象,允许注解处理器访问和操作源代码中的各种元素。
Types getTypeUtils():获取用于处理类型信息的Types对象,允许注解处理器执行类型检查、类型转换等操作。
Map:获取注解处理器的选项集合,允许注解处理器获取外部传递的参数和选项。getOptions()
Locale getLocale():获取当前环境的语言设置,可以用于在不同语言环境下生成本地化的消息。
RoundEnvironment
在 Java 注解处理器中,
RoundEnvironment是一个用于表示当前编译轮次的对象,它提供了有关编译期间的注解信息和元素的访问接口。注解处理器在每个编译轮次都会被调用一次,并且每轮的RoundEnvironment对象都会提供当前轮次中的注解和元素信息。每一轮的
RoundEnvironment对象会包含一组元素,这些元素是被处理器所关注的注解所标注的程序元素(例如类、方法、字段等)。通过RoundEnvironment,注解处理器可以获取到在当前轮次中被标注的元素,并进行相应的处理。下面是一些常见的
RoundEnvironment方法和用途:
getElementsAnnotatedWith(Class extends Annotation> a):返回一个被给定注解标注的元素集合。通过传递注解的类对象,可以获取到被标注的类、方法、字段等元素。
getElementsAnnotatedWith(TypeElement a):与上述方法类似,但是接受一个TypeElement参数。
getElementsAnnotatedWith(javax.lang.model.element.ElementKind kind):返回指定元素类型的元素集合,例如ElementKind.CLASS、ElementKind.METHOD等。
getRootElements():返回当前轮次中所有被处理的根元素,通常是源文件中的类。
ElementKind
javax.lang.model.element.ElementKind是 Java 注解处理器中的一个枚举类型,用于表示程序元素(element)的种类或类型。程序元素是源代码中的各种构造,如类、接口、方法、字段等。ElementKind枚举为每种元素提供了一个常量,帮助注解处理器在处理过程中判断和识别不同类型的元素。以下是
ElementKind中的一些常见元素种类和对应的常量:
PACKAGE:表示包(package)元素。
ENUM:表示枚举类型(enum)元素。
CLASS:表示类(class)元素。
INTERFACE:表示接口(interface)元素。
ANNOTATION_TYPE:表示注解类型(annotation type)元素。
ENUM_CONSTANT:表示枚举常量(enum constant)元素。
FIELD:表示字段(field)元素。
PARAMETER:表示方法或构造函数的参数(parameter)元素。
LOCAL_VARIABLE:表示局部变量(local variable)元素。
EXCEPTION_PARAMETER:表示异常参数(exception parameter)元素。
METHOD:表示方法(method)元素。
CONSTRUCTOR:表示构造函数(constructor)元素。
STATIC_INIT:表示静态初始化块(static initializer)元素。
INSTANCE_INIT:表示实例初始化块(instance initializer)元素。
OTHER:表示其他未知类型的元素。通过使用
ElementKind,注解处理器可以在处理不同类型的元素时执行不同的逻辑。例如,你可以根据元素的种类来判断是否生成额外的代码,或者是否进行特定类型的静态分析。以下是一个简单的示例,演示如何使用
ElementKind判断不同类型的元素:import javax.annotation.processing.*; import javax.lang.model.element.*; import javax.lang.model.SourceVersion; import java.util.Set;@SupportedAnnotationTypes("*") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ElementKindExample extends AbstractProcessor {@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {for (Element element : roundEnv.getRootElements()) {ElementKind kind = element.getKind();System.out.println("Element: " + element.getSimpleName() + ", Kind: " + kind);if (kind == ElementKind.CLASS) {System.out.println("Found class: " + element.getSimpleName());} else if (kind == ElementKind.METHOD) {System.out.println("Found method: " + element.getSimpleName());} else if (kind == ElementKind.FIELD) {System.out.println("Found field: " + element.getSimpleName());}}return true;} }在上述示例中,
ElementKindExample注解处理器遍历所有根元素,并根据元素的种类输出相应的信息。这有助于你了解不同类型的元素在注解处理器中的表现和处理方式。通过熟悉和使用
ElementKind,你可以更好地理解和操作程序元素,从而在注解处理器中实现各种功能。
ExecutableElement
javax.lang.model.element.ExecutableElement是 Java 注解处理器中的一个接口,它表示可执行元素,即方法和构造函数。ExecutableElement继承自Element接口,提供了访问和操作方法和构造函数的属性和信息的方法。在注解处理器中,你可以使用
ExecutableElement来获取方法和构造函数的相关信息,例如方法名、参数、返回类型、注解等。以下是
ExecutableElement接口中的一些常用方法:
List extends VariableElement> getParameters():获取方法或构造函数的参数列表。
TypeMirror getReturnType():获取方法的返回类型。
List extends TypeMirror> getThrownTypes():获取方法可能抛出的异常类型列表。
TypeElement getEnclosingElement():获取包含该方法或构造函数的元素,通常是类或接口。
boolean isVarArgs():检查方法是否为可变参数方法。
boolean isDefault():检查方法是否为接口的默认方法。
List extends TypeParameterElement> getTypeParameters():获取方法或构造函数的类型参数列表。
List extends TypeMirror> getThrownTypes():获取方法可能抛出的异常类型列表。
ExecutableType asType():将此ExecutableElement转换为ExecutableType,用于进行类型检查。
TypeElement
javax.lang.model.element.TypeElement是 Java 注解处理器中的一个接口,它表示程序元素中的类或接口。TypeElement继承自Element接口,提供了访问和操作类和接口的属性和信息的方法。在注解处理器中,你可以使用
TypeElement来获取类和接口的相关信息,例如类名、父类、实现的接口、内部类、注解等。以下是
TypeElement接口中的一些常用方法:
Name getQualifiedName():获取类或接口的限定名,以Name对象表示。
TypeMirror getSuperclass():获取类的父类的类型。
List extends TypeMirror> getInterfaces():获取类或接口实现的接口的类型列表。
List extends Element> getEnclosedElements():获取类或接口中的成员元素,如字段、方法、内部类等。
TypeMirror asType():将此TypeElement转换为DeclaredType,用于进行类型检查。
boolean isInterface():检查是否为接口。
boolean isEnum():检查是否为枚举类型。
boolean isAnnotation():检查是否为注解类型。
boolean isClass():检查是否为类类型。
Messager
javax.annotation.processing.Messager是 Java 注解处理器中的一个接口,它提供了向注解处理器的使用者(通常是开发者)报告消息、警告和错误的能力。通过Messager,注解处理器可以向编译器输出信息,帮助开发者了解代码处理过程中的情况,以及可能需要修复的问题。
Messager接口定义了一些方法,允许注解处理器输出消息、警告和错误。以下是Messager接口中常用的方法:
void printMessage(Diagnostic.Kind kind, CharSequence msg):输出一条消息,可以是信息、警告或错误。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e):输出一条消息,并指定与该消息关联的元素。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a):输出一条消息,并指定与该消息关联的元素和注解镜像。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v):输出一条消息,并指定与该消息关联的元素、注解镜像和注解值。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v, AnnotationValue v1):输出一条消息,并指定与该消息关联的元素、注解镜像、注解值等。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v, AnnotationValue v1, AnnotationValue v2):输出一条消息,并指定与该消息关联的元素、注解镜像、注解值等。在编写注解处理器时,通常会使用
Messager输出信息、警告和错误,以便在编译时向开发者传达处理结果和可能的问题。下面是一个简单的示例,展示了如何在注解处理器中使用Messager输出消息和警告:
作用到类上捕获异常
package com.jct.demo.processor;/*** CatchAndLogProcessor* * Description* * Creation Time: 2023/8/14.** @author zhennan.Xu* @since terry-boot*/import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Names;import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;@SupportedAnnotationTypes("com.jct.demo.processor.CatchAndLog")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CatchAndLogProcessor extends AbstractProcessor {private Trees trees;protected Messager messager;// 封装了创建AST节点的一些方法protected TreeMaker treeMaker;// 提供了创建标识符的方法protected Names names;/*** 用来处理Element的工具类* Elements接口的对象,用于操作元素的工具类。*/private JavacElements elementUtils;@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);trees = Trees.instance(processingEnv);this.trees = JavacTrees.instance(processingEnv);Context context = ((JavacProcessingEnvironment) processingEnv).getContext();this.treeMaker = TreeMaker.instance(context);this.names = Names.instance(context);messager = processingEnv.getMessager();elementUtils = (JavacElements) processingEnv.getElementUtils();}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {System.out.println("进入process");messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解处理器处理中");TypeElement currentAnnotation = null;// 遍历注解集合,也即@SupportedAnnotationTypes中标注的类型for (TypeElement annotation : annotations) {messager.printMessage(Diagnostic.Kind.NOTE, "遍历本注解处理器处理的所有注解,当前遍历到的注解是:" + annotation.getSimpleName());currentAnnotation = annotation;}Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(CatchAndLog.class);messager.printMessage(Diagnostic.Kind.NOTE, "CatchAndLogProcessor注解处理器处理@CatchAndLog注解");for (Element element : elementSet) {if (element.getKind() == ElementKind.METHOD) {JCTree tree = (JCTree) trees.getTree(element);System.out.println("进入循环");if (tree instanceof JCTree.JCMethodDecl) {JCTree.JCMethodDecl methodDecl = (JCTree.JCMethodDecl) tree;// 方法名称String methodName = element.getSimpleName().toString();// 类的全限定名称String className = element.getEnclosingElement().toString();messager.printMessage(Diagnostic.Kind.NOTE, "当前被标注注解的方法所在的类是:" + className);messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName().toString() + "当前被标注注解的方法是:" + methodName);enhanceMethodDecl(methodDecl);}}}return true;}private void enhanceMethodDecl(JCTree.JCMethodDecl methodDecl) {//try {////}catch (Exception ex) {// ex.printStackTrace();//}JCTree.JCBlock tryBlock = methodDecl.body;JCTree.JCVariableDecl exVar = createVarDef(treeMaker.Modifiers(0),"ex",treeMaker.Ident(names.fromString("Exception")),null);JCTree.JCBlock catchBlock = treeMaker.Block(0L, List.of(treeMaker.Exec(treeMaker.Apply(List.nil(),treeMaker.Select(treeMaker.Ident(names.fromString("ex")),names.fromString("printStackTrace")),List.nil()))));JCTree.JCCatch catchClause = treeMaker.Catch(exVar, catchBlock);JCTree.JCTry tryCatchTree = treeMaker.Try(tryBlock, List.of(catchClause), null);methodDecl.body = treeMaker.Block(0L, List.of(tryCatchTree));System.out.println(methodDecl.body.toString());}private com.sun.tools.javac.util.Name getNameFromString(String s) {return names.fromString(s);}/*** 创建变量语句** @param modifiers* @param name 变量名* @param varType 变量类型* @param init 变量初始化语句* @return*/private JCTree.JCVariableDecl createVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression varType, JCTree.JCExpression init) {return treeMaker.VarDef(modifiers,//名字getNameFromString(name),//类型varType,//初始化语句init);}
}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
