diff --git a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderBlockIf.java b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderBlockIf.java index 7db7a1099..29cb8dbf6 100644 --- a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderBlockIf.java +++ b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderBlockIf.java @@ -28,7 +28,9 @@ import oap.template.runtime.RuntimeContext; import javax.annotation.Nullable; +import java.util.HashSet; import java.util.List; +import java.util.Set; @ToString( callSuper = true ) public class AstRenderBlockIf extends AstRender { @@ -52,9 +54,11 @@ public void render( Render render ) { conditionAst.render( render.withBooleanIfVar( condVar ) ); + Set thenNullSafe = collectNullRequired( conditionAst, render ); + render.ntab().append( "if ( %s ) {", condVar ); - Render thenBodyRender = render.tabInc().newBlock(); + Render thenBodyRender = render.tabInc().newBlock().withNullVerified( thenNullSafe ); for( AstRender child : thenChildren ) { child.render( thenBodyRender ); } @@ -73,6 +77,19 @@ public void render( Render render ) { } } + private static Set collectNullRequired( AstRender node, Render render ) { + Set result = new HashSet<>(); + if( node instanceof AstRenderField f + && f.children.size() == 1 + && f.children.getFirst() instanceof AstRenderNullable ) { + result.add( render.peekVariableName( f.fieldName ) ); + } else if( node instanceof AstRenderConditionAnd and ) { + result.addAll( collectNullRequired( and.left, render ) ); + result.addAll( collectNullRequired( and.right, render ) ); + } + return result; + } + @Override public void interpret( RuntimeContext ctx ) { boolean[] capture = { false }; diff --git a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderConditionAnd.java b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderConditionAnd.java index 6922e2ab4..cb8c5e837 100644 --- a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderConditionAnd.java +++ b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderConditionAnd.java @@ -29,8 +29,8 @@ @ToString( callSuper = true ) public class AstRenderConditionAnd extends AstRender { - private final AstRender left; - private final AstRender right; + final AstRender left; + final AstRender right; public AstRenderConditionAnd( TemplateType type, AstRender left, AstRender right ) { super( type ); @@ -40,6 +40,51 @@ public AstRenderConditionAnd( TemplateType type, AstRender left, AstRender right @Override public void render( Render render ) { + String sharedField = sharedTopLevelField(); + if( sharedField != null ) + renderMerged( render, sharedField ); + else + renderDefault( render ); + } + + private String sharedTopLevelField() { + String lf = topLevelFieldName( left ); + String rf = topLevelFieldName( right ); + return lf != null && lf.equals( rf ) ? lf : null; + } + + private static String topLevelFieldName( AstRender node ) { + if( !( node instanceof AstRenderField f ) ) return null; + if( f.children.size() != 1 ) return null; + if( !( f.children.getFirst() instanceof AstRenderNullable ) ) return null; + return f.fieldName; + } + + private void renderMerged( Render render, String fieldName ) { + String leftVar = render.newVariable(); + String rightVar = render.newVariable(); + render.ntab().append( "boolean %s = false;", leftVar ); + render.ntab().append( "boolean %s = false;", rightVar ); + + AstRenderField leftField = ( AstRenderField ) left; + AstRenderNullable leftNullable = ( AstRenderNullable ) leftField.children.getFirst(); + AstRenderField rightField = ( AstRenderField ) right; + AstRenderNullable rightNullable = ( AstRenderNullable ) rightField.children.getFirst(); + + Render.NewVariable sv = render.newVariable( fieldName ); + if( sv.isNew ) + render.ntab().append( "%s %s = %s.%s;", leftField.type.getTypeName(), sv.name, render.field, fieldName ); + + render.ntab().append( "if ( %s != null ) {", sv.name ); + Render inner = render.withField( sv.name ).withParentType( leftField.type ).tabInc(); + leftNullable.renderBodyOnly( inner.withBooleanIfVar( leftVar ).newBlock() ); + rightNullable.renderBodyOnly( inner.withBooleanIfVar( rightVar ).newBlock() ); + render.ntab().append( "}" ); + + render.ntab().append( "%s = %s && %s;", render.booleanIfVar, leftVar, rightVar ); + } + + private void renderDefault( Render render ) { String leftVar = render.newVariable(); String rightVar = render.newVariable(); render.ntab().append( "boolean %s = false;", leftVar ); diff --git a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderNullable.java b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderNullable.java index f8773b2d8..db7eea3ae 100644 --- a/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderNullable.java +++ b/oap-formats/oap-template/src/main/java/oap/template/render/AstRenderNullable.java @@ -35,6 +35,15 @@ public AstRenderNullable( TemplateType type ) { super( type ); } + @Override + public void render( Render render ) { + if( render.nullVerifiedVars.contains( render.field ) ) { + renderBodyOnly( render ); + return; + } + super.render( render ); + } + @Override protected String getTrue() { return " != null"; diff --git a/oap-formats/oap-template/src/main/java/oap/template/render/Render.java b/oap-formats/oap-template/src/main/java/oap/template/render/Render.java index ecbc9665d..4f258e1d2 100644 --- a/oap-formats/oap-template/src/main/java/oap/template/render/Render.java +++ b/oap-formats/oap-template/src/main/java/oap/template/render/Render.java @@ -33,6 +33,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @ToString @@ -50,6 +51,7 @@ public class Render { public final String rootField; public final String prefix; public final Map rangeVarMap; + public final Set nullVerifiedVars; private final AtomicInteger ids; private final StringBuilder sb; private final ArrayDeque> variables; @@ -61,13 +63,13 @@ private Render( String templateName, String content, TemplateType parentType, Te { this.addFirst( new HashSet<>() ); } - } ); + }, Set.of() ); } public Render( StringBuilder sb, String templateName, String content, TemplateType parentType, TemplateAccumulator templateAccumulator, String field, String templateAccumulatorName, int tab, AtomicInteger ids, String tryVariable, String booleanIfVar, String scopeVar, String rootField, String prefix, - Map rangeVarMap, ArrayDeque> variables ) { + Map rangeVarMap, ArrayDeque> variables, Set nullVerifiedVars ) { this.sb = sb; this.templateName = templateName; this.content = content; @@ -84,6 +86,7 @@ public Render( StringBuilder sb, String templateName, String content, TemplateTy this.prefix = prefix; this.rangeVarMap = rangeVarMap; this.variables = variables; + this.nullVerifiedVars = nullVerifiedVars; } public static Render init( String templateName, String content, TemplateType type, TemplateAccumulator acc ) { @@ -92,32 +95,32 @@ public static Render init( String templateName, String content, TemplateType typ public Render withField( String field ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, variableNameWithPrefix( field ), rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, variableNameWithPrefix( field ), rangeVarMap, variables, nullVerifiedVars ); } public Render withContent( String content ) { return new Render( this.sb, this.templateName, content, this.parentType, this.templateAccumulator, field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withTemplateAccumulatorName( String templateAccumulatorName ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render tabInc() { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab + 1, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab + 1, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render tabDec() { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab - 1, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab - 1, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withParentType( TemplateType parentType ) { return new Render( this.sb, this.templateName, this.content, parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render n() { @@ -195,34 +198,39 @@ public NewVariable newVariable( String name ) { public Render withTryVariable( String tryVariable ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withBooleanIfVar( String booleanIfVar ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withScopeVar( String scopeVar ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withFieldDirect( String field ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, field, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, field, rangeVarMap, variables, nullVerifiedVars ); } public Render withRootField( String rootField ) { return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, nullVerifiedVars ); } public Render withRangeVar( String name, String javaName ) { var newMap = new HashMap<>( rangeVarMap ); newMap.put( name, javaName ); return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, newMap, variables ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, newMap, variables, nullVerifiedVars ); + } + + public Render withNullVerified( Set vars ) { + return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, variables, vars ); } public Render newBlock() { @@ -233,7 +241,13 @@ public Render newBlock() { newStack.addFirst( new HashSet<>() ); return new Render( this.sb, this.templateName, this.content, this.parentType, this.templateAccumulator, this.field, - this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, newStack ); + this.templateAccumulatorName, this.tab, ids, tryVariable, booleanIfVar, scopeVar, rootField, prefix, rangeVarMap, newStack, nullVerifiedVars ); + } + + @SuppressWarnings( "checkstyle:ParameterAssignment" ) + public String peekVariableName( String name ) { + name = name.replaceAll( "[\\s,?\\-]", "_" ); + return variableNameWithPrefix( name ); } private String variableNameWithPrefix( String name ) { diff --git a/oap-formats/oap-template/src/test/java/oap/template/TemplateEngineOptimizationTest.java b/oap-formats/oap-template/src/test/java/oap/template/TemplateEngineOptimizationTest.java index a28698aa2..93849f31d 100644 --- a/oap-formats/oap-template/src/test/java/oap/template/TemplateEngineOptimizationTest.java +++ b/oap-formats/oap-template/src/test/java/oap/template/TemplateEngineOptimizationTest.java @@ -1,6 +1,7 @@ package oap.template; import oap.reflect.TypeRef; +import org.apache.commons.lang3.StringUtils; import org.testng.annotations.Test; import static oap.template.TemplateAccumulators.STRING; @@ -17,6 +18,8 @@ public void testBlockWithMultipleFields() { "{{% with child }}{{ field }}-{{ field2 }}{{% end }}", STRING, null ).render( c ).get() ) .isEqualTo( "f1-f2" ); + assertThat( StringUtils.countMatches( listener.javaCode, "if " ) ).isEqualTo( 3 ); + assertThat( listener.javaCode ) .containsOnlyOnce( """ oap.template.TestTemplateClass s_child = s.child; @@ -42,6 +45,23 @@ public void testBlockWithMultipleFieldsAndComments() { "{{% with child }}{{ /** c field */ field }}-{{ /** c field2 */field2 }}{{% end }}", STRING, null ).render( c ).get() ) .isEqualTo( "f1-f2" ); + assertThat( StringUtils.countMatches( listener.javaCode, "if " ) ).isEqualTo( 3 ); + assertThat( listener.javaCode ).containsOnlyOnce( "if ( s_child != null ) {" ); } + + @Test + public void testBlockIfOptimization() { + TestTemplateClass c = new TestTemplateClass(); + c.child = new TestTemplateClass(); + c.child.field = "1"; + c.child.field2 = "2"; + assertThat( getTemplate( testMethodName + "Both", new TypeRef() {}, + "{{% if child.field and child.field2 }}{{ child.field }}{{% else }}no{{% end }}", STRING, null ).render( c ).get() ) + .isEqualTo( "1" ); + + assertThat( StringUtils.countMatches( listener.javaCode, "if " ) ).isEqualTo( 5 ); + + assertThat( listener.javaCode ).containsOnlyOnce( "if ( s_child != null )" ); + } } diff --git a/pom.xml b/pom.xml index 6001d9787..d7f3301b6 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ - 25.6.4 + 25.6.5 25.0.1 25.0.0