Randgalt / record-builder

Record builder generator for Java records
Apache License 2.0
758 stars 55 forks source link

Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null #139

Open youribonnaffe opened 1 year ago

youribonnaffe commented 1 year ago

We use record-builder on a project and we often get compilation errors with the following message: Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null

The errors happen either in Intellij IDEA or via the CLI with Maven. Running a re-build in Intellij or running the command again fixes the issue (usually a mvn verify).

I suspect it could come from the two tools using a different JDK or compilation options and each generating slightly different bytecode. However I've made sure that the same JDK (17.0.6) is used in both cases.

I've managed to capture a stack trace below. I'm not confident about being able to provide a reproducer project but if the error message is not helpful in understanding the problem I can give it a shot.

Caused by: java.lang.NullPointerException: Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.lambda$buildRecordComponents$2 (InternalRecordBuilderProcessor.java:143)
    at java.util.stream.ReferencePipeline$3$1.accept (ReferencePipeline.java:197)
    at java.util.Iterator.forEachRemaining (Iterator.java:133)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining (Spliterators.java:1845)
    at java.util.stream.AbstractPipeline.copyInto (AbstractPipeline.java:509)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:499)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential (ReduceOps.java:921)
    at java.util.stream.AbstractPipeline.evaluate (AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect (ReferencePipeline.java:682)
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.buildRecordComponents (InternalRecordBuilderProcessor.java:143)
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.<init> (InternalRecordBuilderProcessor.java:66)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.processRecordBuilder (RecordBuilderProcessor.java:168)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.process (RecordBuilderProcessor.java:76)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.lambda$process$0 (RecordBuilderProcessor.java:54)
    at java.lang.Iterable.forEach (Iterable.java:75)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.lambda$process$1 (RecordBuilderProcessor.java:54)
    at java.lang.Iterable.forEach (Iterable.java:75)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.process (RecordBuilderProcessor.java:54)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor (JavacProcessingEnvironment.java:1023)
    ....
Randgalt commented 1 year ago

I'll see what I can do. But, please provide a reproducible case.

cykl commented 1 year ago

Youri co-worker here. I don't have a reproducer yet but I have been able to use a debugger to get a glimpse of what is going on.

We use @RecordInterface to define a record from an interface

@RecordInterface
interface Conf {

    int maxConcurrency( );
}

RecordBuilder generate the following class

@Generated("io.soabase.recordbuilder.core.RecordInterface")
@RecordBuilder
public record ConfRecord(int maxConcurrency) implements Conf, ConfRecordBuilder.With {}

I'm able to observe that the TypeElement argument passed to buildRecordComponents correspond to ConfRecord. I also observe that maxConcurrency's accessor is null.

image

e.getRecordComponents() returns two entries. Both are named maxConcurrency. The first one has an accessor, the second one doesn't. At this point, it is unclear to me if this is what is expected from getRecordComponents. I think not, but have to check that.

I see three options:

  1. This is expected and recordComponents without accessor should be ignored
  2. This is unexpected and triggered by record-builder and we must understand why
  3. This is unexpected and a a bug in JRE / javac that must be identified and reported
cykl commented 1 year ago

I just checked com.sun.tools.javac.code.Symbol.ClassSymbol#getRecordComponent(com.sun.tools.javac.tree.JCTree.JCVariableDecl, boolean, com.sun.tools.javac.util.List) implementation (Zulu 17.0.4)

        public RecordComponent getRecordComponent(JCVariableDecl var, boolean addIfMissing, List<JCAnnotation> annotations) {
            RecordComponent toRemove = null;
            for (RecordComponent rc : recordComponents) {
                /* it could be that a record erroneously declares two record components with the same name, in that
                 * case we need to use the position to disambiguate
                 */
                if (rc.name == var.name && var.pos == rc.pos) {
                    if (rc.type.hasTag(TypeTag.ERROR) && !var.sym.type.hasTag(TypeTag.ERROR)) {
                        // Found a record component with an erroneous type: save it so that it can be removed later.
                        // If the class type of the record component is generated by annotation processor, it should
                        // use the new actual class type and symbol instead of the old dummy ErrorType.
                        toRemove = rc;
                    } else {
                        // Found a good record component: just return.
                        return rc;
                    }
                }
            }
            RecordComponent rc = null;
            if (toRemove != null) {
                // Found a record component with an erroneous type: remove it and create a new one
                recordComponents = List.filter(recordComponents, toRemove);
                recordComponents = recordComponents.append(rc = new RecordComponent(var.sym, annotations));
            } else if (addIfMissing) {
                // Didn't find the record component: create one.
                recordComponents = recordComponents.append(rc = new RecordComponent(var.sym, annotations));
            }
            return rc;
        }

It seems to avoid registering duplicates unless (name, pos) don't match. I observe that the entry related to the record has pos = -1 but the entry related to the interface has pos = 387.

It is also unclear why it work sometime and sometimes it doesn't. I figured out that if I edit an unrelated file in IntelliJ then run a Maven build it triggers the issue but I'm unsure why. I was able to check that the md5sum of the record class doesn't change. Perhaps maven / javac is able to not run the annotation processor if nothing changed on disk?

cykl commented 1 year ago

@Randgalt FYI I created https://stackoverflow.com/questions/75290864/record-builder-typeelement-getrecordcomponents-returns-duplicated-record-com.

cykl commented 1 year ago

@Randgalt Here is a reproducer https://github.com/cykl/record-builder-139-reproducer

Randgalt commented 1 year ago

Thanks for this. I'll get to it soon.

Randgalt commented 1 year ago

@cykl I'm unable to reproduce the problem. Can you give more details on your environment? I tried with java 17 and 19 and the example project you provided. I don't get any errors.

cykl commented 1 year ago

Thanks for giving it a try. We deterministically get an error using Java 17, Maven 3.8 on Linux (Fedora / Ubuntu) & macOS.

Here is the output on my laptop:

$ java -version
openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment (Red_Hat-17.0.5.0.8-3.fc36) (build 17.0.5+8)
OpenJDK 64-Bit Server VM (Red_Hat-17.0.5.0.8-3.fc36) (build 17.0.5+8, mixed mode, sharing)

$ mvn --version 
Apache Maven 3.8.4 (Red Hat 3.8.4-3)
Maven home: /usr/share/maven
Java version: 17.0.5, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-17-openjdk-17.0.5.0.8-3.fc36.x86_64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.1.6-100.fc36.x86_64", arch: "amd64", family: "unix"

$ ./trigger.sh 
+ git checkout .
Updated 1 path from the index
+ mvn clean compile
[INFO] Scanning for projects...
[INFO] 
[INFO] -------------< org.example:record-builder-139-reproducer >--------------
[INFO] Building record-builder-139-reproducer 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ record-builder-139-reproducer ---
[INFO] Deleting /home/cmathieu/sources/record-builder-139-reproducer/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ record-builder-139-reproducer ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/cmathieu/sources/record-builder-139-reproducer/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ record-builder-139-reproducer ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/cmathieu/sources/record-builder-139-reproducer/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.308 s
[INFO] Finished at: 2023-02-18T09:36:52+01:00
[INFO] ------------------------------------------------------------------------
+ sed -i s/10/11/ src/main/java/Main.java
+ rm target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+ mvn compile
[INFO] Scanning for projects...
[INFO] 
[INFO] -------------< org.example:record-builder-139-reproducer >--------------
[INFO] Building record-builder-139-reproducer 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ record-builder-139-reproducer ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/cmathieu/sources/record-builder-139-reproducer/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ record-builder-139-reproducer ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/cmathieu/sources/record-builder-139-reproducer/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.903 s
[INFO] Finished at: 2023-02-18T09:36:54+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project record-builder-139-reproducer: Fatal error compiling: java.lang.NullPointerException: Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
Randgalt commented 1 year ago

I just pushed branch jordanz/check-null-accessor - it does a null check. Can you test with this?

youribonnaffe commented 1 year ago

Using this branch, the execution of trigger.sh fails with:

[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ record-builder-139-reproducer ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to ./record-builder-139-reproducer/target/classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[15,17] variable maxConcurrency is already defined in class pkg1.ConfRecordBuilder
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[22,55] variable maxConcurrency is already defined in constructor ConfRecordBuilder(int,int)
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[31,65] variable maxConcurrency is already defined in method ConfRecord(int,int)
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[117,30] method maxConcurrency(int) is already defined in class pkg1.ConfRecordBuilder
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[126,16] method maxConcurrency() is already defined in class pkg1.ConfRecordBuilder
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[145,13] method maxConcurrency() is already defined in interface pkg1.ConfRecordBuilder.With
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[177,28] method withMaxConcurrency(int) is already defined in interface pkg1.ConfRecordBuilder.With
[ERROR] ./record-builder-139-reproducer/target/generated-sources/annotations/pkg1/ConfRecordBuilder.java:[199,20] method maxConcurrency() is already defined in class pkg1.ConfRecordBuilder._FromWith
[INFO] 8 errors
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
Randgalt commented 1 year ago

Thanks for the update @youribonnaffe - I'll spend more time on this when I can and see if I can get to the bottom of it.

Randgalt commented 1 year ago

Some potentially helpful news: it turns out this reproduces in IntelliJ sometimes. If I open RecordBuilder fresh in IntelliJ and run I see the same error. So, at least I have a semi-reproducible case.

Randgalt commented 1 year ago

I can now reliably reproduce this in IntelliJ by single-compiling any interface annotated with @RecordInterface. i.e. in IntelliJ, open BeanStyle.java and select Recompile 'BeanStyle.java' from the Build menu.

Randgalt commented 1 year ago

@youribonnaffe and @cykl As I mention above, I can now reliably reproduce the issue in IntelliJ. The problem seems to be subsequent rounds of annotation processing. javac is supposed to process the generated record from @RecordInterface but there seems to be bugs with this. As an experiment, I have a branch that has the @RecordInterface handler write the record and the record builder at the same time. The record does not get the @RecordBuilder annotation and, so, javac won't try to process it on the next round. This appears to fix the issue, however I have seen some other javac bugs.

Can you please test with this branch and let me know your results?

jordanz/test-manually-build-interface-record-builder

youribonnaffe commented 1 year ago

Thanks for taking another look at it. I tried with the reproducer and the branch jordanz/test-manually-build-interface-record-builder, the trigger.sh script fails with:

An exception has occurred in the compiler (17.0.6). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.
java.lang.NullPointerException: Cannot invoke "com.sun.tools.javac.code.Symbol$MethodSymbol.flags()" because "rc.accessor" is null
    at jdk.compiler/com.sun.tools.javac.comp.Lower.lambda$generateMandatedAccessors$4(Lower.java:2255)
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178)
    at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1845)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.generateMandatedAccessors(Lower.java:2262)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.visitRecordDef(Lower.java:2459)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.visitClassDef(Lower.java:2180)
    at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCClassDecl.accept(JCTree.java:819)
    at jdk.compiler/com.sun.tools.javac.tree.TreeTranslator.translate(TreeTranslator.java:58)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.translate(Lower.java:2072)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.translate(Lower.java:2091)
    at jdk.compiler/com.sun.tools.javac.comp.Lower.translateTopLevelClass(Lower.java:4138)
    at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.desugar(JavaCompiler.java:1561)
    at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.desugar(JavaCompiler.java:1408)
    at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:946)
    at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.lambda$doCall$0(JavacTaskImpl.java:104)
    at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.invocationHelper(JavacTaskImpl.java:152)
    at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:100)
    at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:94)
    at org.codehaus.plexus.compiler.javac.JavaxToolsCompiler.compileInProcess(JavaxToolsCompiler.java:136)
    at org.codehaus.plexus.compiler.javac.JavacCompiler.performCompile(JavacCompiler.java:182)
    at org.apache.maven.plugin.compiler.AbstractCompilerMojo.execute(AbstractCompilerMojo.java:1209)
    at org.apache.maven.plugin.compiler.CompilerMojo.execute(CompilerMojo.java:198)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:137)
    at org.apache.maven.lifecycle.internal.MojoExecutor.doExecute2(MojoExecutor.java:370)
    at org.apache.maven.lifecycle.internal.MojoExecutor.doExecute(MojoExecutor.java:351)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:215)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:171)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:163)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:117)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:81)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build(SingleThreadedBuilder.java:56)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:128)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:298)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:192)
    at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:105)
    at org.apache.maven.cli.MavenCli.execute(MavenCli.java:960)
    at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:293)
    at org.apache.maven.cli.MavenCli.main(MavenCli.java:196)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:282)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:225)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:406)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:347)

I also tried with jordanz/delete-vestigal-class-file-for-record-interface but this one fails with the original error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project record-builder-139-reproducer: Fatal error compiling: java.lang.NullPointerException: Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null
Randgalt commented 1 year ago

Man - it seems javac is hopeless broken with what record-builder is doing here. :( I'm not sure how to handle this.

@youribonnaffe out of curiosity. If you change https://github.com/cykl/record-builder-139-reproducer/blob/main/trigger.sh to do mvn clean compile with the released record-builder does it still fail?

youribonnaffe commented 1 year ago

@Randgalt sorry it seems I missed the last message.

If you change https://github.com/cykl/record-builder-139-reproducer/blob/main/trigger.sh to do mvn clean compile with the released record-builder does it still fail?

You mean changing the second mvn command to clean compile right? In that case it does not fail. But that would be expected as right now cleaning all the generated code fixes the issue.

KrzysztofDzioba commented 10 months ago

Workaround for Intellj Idea (2022.3):

On Run/Debug configuration, click "Edit Configuration..." -> Modify options -> Do not build before run

Remember to compile project before running test

Randgalt commented 8 months ago

I realize you've probably moved away from RecordBuilder at this point, but I may have a fix for this issue: https://github.com/Randgalt/record-builder/pull/172

cykl commented 8 months ago

Thanks @Randgalt! We haven't moved away but stopped using RecordInterface until it gets resolved. I will try the fix one of those days.

Randgalt commented 8 months ago

Thanks @Randgalt! We haven't moved away but stopped using RecordInterface until it gets resolved. I will try the fix one of those days.

I'd really appreciate it if you can test with this fix. My solution (I don't know why I didn't think of it before) is to manually find the accessor method instead of use getAccessor().

Update: it solves one problem but causes others it seems. javac just can't seem to handle generated records for some reason. I'll keep seeing what I can do

Randgalt commented 8 months ago

@cykl I think I really have it now. If you can, please test with https://github.com/Randgalt/record-builder/pull/174 - in my local testing it solves the problem.

youribonnaffe commented 8 months ago

I gave the reproducer a shot with #174 and unfortunately I still see the same error.

Randgalt commented 8 months ago

Damn - thanks. I posted a bug report with Oracle. Let's see what comes of it.

Randgalt commented 8 months ago

@youribonnaffe FYI - I created a container and am able to reproduce using the tester provided in the container (but still not in MacOS). There is a workaround until the bug is fixed in javac. Cleaning the generated sources first fixes the problem though obviously not ideal. Here's a the Maven config for it:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-clean-plugin</artifactId>
    <version>3.3.2</version>
    <executions>
        <execution>
            <id>clean-generated</id>
            <phase>validate</phase>
            <goals>
                <goal>clean</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <excludeDefaultDirectories>true</excludeDefaultDirectories>
        <filesets>
            <fileset>
                <directory>${project.build.directory}/generated-sources/annotations</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </fileset>
        </filesets>
    </configuration>
</plugin>