diff --git a/src/main/java/ortus/boxlang/runtime/components/system/Exit.java b/src/main/java/ortus/boxlang/runtime/components/system/Exit.java new file mode 100644 index 000000000..558841211 --- /dev/null +++ b/src/main/java/ortus/boxlang/runtime/components/system/Exit.java @@ -0,0 +1,79 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * 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 ortus.boxlang.runtime.components.system; + +import java.util.Set; + +import ortus.boxlang.runtime.components.Attribute; +import ortus.boxlang.runtime.components.BoxComponent; +import ortus.boxlang.runtime.components.Component; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.exceptions.AbortException; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; +import ortus.boxlang.runtime.validation.Validator; + +@BoxComponent +public class Exit extends Component { + + /** + * -------------------------------------------------------------------------- + * Constructor(s) + * -------------------------------------------------------------------------- + */ + + public Exit() { + super(); + declaredAttributes = new Attribute[] { + new Attribute( Key.method, "string", "exitTag", Set.of( Validator.valueOneOf( "exitTag", "exitTemplate", "loop" ) ) ), + }; + } + + /** + * This component aborts processing of the currently executing custom tag, exits the page within the currently executing custom tag, or re-executes a section of code within the currently executing custom tag. + * + * @param context The context in which the Component is being invoked + * @param attributes The attributes to the Component + * @param body The body of the Component + * @param executionState The execution state of the Component + * + * @attribute.method The method to use for exiting (exitTag, exitTemplate, loop) + * + */ + public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBody body, IStruct executionState ) { + String method = attributes.getAsString( Key.method ).toLowerCase(); + + String type; + switch ( method ) { + case "exittag" : + type = "exit-tag"; + break; + case "exittemplate" : + type = "exit-template"; + break; + case "loop" : + type = "exit-loop"; + break; + default : + throw new BoxValidationException( "Invalid exit method: " + method ); + } + context.flushBuffer( false ); + throw new AbortException( type, null ); + } +} diff --git a/src/main/java/ortus/boxlang/runtime/components/system/Module.java b/src/main/java/ortus/boxlang/runtime/components/system/Module.java index ef79f843f..e9de6a000 100644 --- a/src/main/java/ortus/boxlang/runtime/components/system/Module.java +++ b/src/main/java/ortus/boxlang/runtime/components/system/Module.java @@ -34,7 +34,9 @@ import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; +import ortus.boxlang.runtime.types.exceptions.AbortException; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; import ortus.boxlang.runtime.types.exceptions.CustomException; import ortus.boxlang.runtime.util.ResolvedFilePath; @@ -109,28 +111,63 @@ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBod variables.put( Key.thisTag, thisTag ); try { - bTemplate.invoke( ctContext ); - ctContext.flushBuffer( false ); + try { + bTemplate.invoke( ctContext ); + } catch ( AbortException e ) { + if ( e.isTag() ) { + return DEFAULT_RETURN; + } else if ( e.isTemplate() || e.isPage() ) { + // Do nothing, we'll contine with the body next like nothing happened + } else if ( e.isLoop() ) { + throw new BoxValidationException( "You cannot use the 'loop' method of the exit component in the start of a custom tag." ); + } else { + // Any other type of abort just keeps going up the stack + throw e; + } + } finally { + ctContext.flushBuffer( false ); + } if ( body != null ) { thisTag.put( Key.executionMode, "inactive" ); - StringBuffer buffer = new StringBuffer(); - BodyResult bodyResult = processBody( context, body, buffer ); - // IF there was a return statement inside our body, we early exit now - if ( bodyResult.isEarlyExit() ) { - // Output thus far - context.writeToBuffer( buffer.toString() ); - return bodyResult; + boolean keepLooping = true; + while ( keepLooping ) { + // Assume we will only exucute the body once + keepLooping = false; + + StringBuffer buffer = new StringBuffer(); + BodyResult bodyResult = processBody( context, body, buffer ); + // IF there was a return statement inside our body, we early exit now + if ( bodyResult.isEarlyExit() ) { + // Output thus far + context.writeToBuffer( buffer.toString() ); + return bodyResult; + } + thisTag.put( Key.generatedContent, buffer.toString() ); + + thisTag.put( Key.executionMode, "end" ); + + try { + bTemplate.invoke( ctContext ); + } catch ( AbortException e ) { + if ( e.isTag() ) { + return DEFAULT_RETURN; + } else if ( e.isTemplate() || e.isPage() ) { + // Do nothing, we'll contine with the body next like nothing happened + } else if ( e.isLoop() ) { + // If the closing tag has exit method="loop", then we will run the tag body again! + keepLooping = true; + } else { + // Any other type of abort just keeps going up the stack + throw e; + } + } finally { + context.writeToBuffer( thisTag.getAsString( Key.generatedContent ) ); + } } - thisTag.put( Key.generatedContent, buffer.toString() ); - - thisTag.put( Key.executionMode, "end" ); - - bTemplate.invoke( ctContext ); - context.writeToBuffer( thisTag.getAsString( Key.generatedContent ) ); } } finally { ctContext.flushBuffer( false ); diff --git a/src/main/java/ortus/boxlang/runtime/components/threading/Thread.java b/src/main/java/ortus/boxlang/runtime/components/threading/Thread.java index 9a965e650..36c00239a 100644 --- a/src/main/java/ortus/boxlang/runtime/components/threading/Thread.java +++ b/src/main/java/ortus/boxlang/runtime/components/threading/Thread.java @@ -149,7 +149,7 @@ private void run( IBoxContext context, String name, String priority, IStruct att processBody( tContext, body, buffer ); } catch ( AbortException e ) { // We log it so we can potentially find out why it was aborted - logger.error( "Thread [{}] aborted at stacktrace: {}", nameKey.getName(), e.getStackTrace() ); + logger.debug( "Thread [{}] aborted at stacktrace: {}", nameKey.getName(), e.getStackTrace() ); } catch ( Throwable e ) { exception = e; logger.error( "Thread [{}] terminated with exception: {}", nameKey.getName(), e.getMessage() ); diff --git a/src/main/java/ortus/boxlang/runtime/runnables/BoxTemplate.java b/src/main/java/ortus/boxlang/runtime/runnables/BoxTemplate.java index d5d7081c4..ae3b9b7e9 100644 --- a/src/main/java/ortus/boxlang/runtime/runnables/BoxTemplate.java +++ b/src/main/java/ortus/boxlang/runtime/runnables/BoxTemplate.java @@ -23,9 +23,11 @@ import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.loader.ImportDefinition; +import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; import ortus.boxlang.runtime.types.exceptions.AbortException; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; import ortus.boxlang.runtime.util.ResolvedFilePath; public abstract class BoxTemplate implements ITemplateRunnable { @@ -43,8 +45,9 @@ public abstract class BoxTemplate implements ITemplateRunnable { * */ public void invoke( IBoxContext context ) { - BoxRuntime runtime = BoxRuntime.getInstance(); - + BoxRuntime runtime = BoxRuntime.getInstance(); + boolean isInModule = context.getComponents().length > 0 + && context.getComponents()[ context.getComponents().length - 1 ].getAsKey( Key._NAME ).equals( Key.module ); context.pushTemplate( this ); try { // Announcements @@ -59,10 +62,17 @@ public void invoke( IBoxContext context ) { // Announce runtime.announce( "postTemplateInvoke", data ); } catch ( AbortException e ) { - context.flushBuffer( true ); - // Swallowing aborts here if type="page" + // Module components have their own checks + if ( isInModule && ( e.isTemplate() || e.isLoop() || e.isTag() ) ) { + throw e; + } + if ( e.isLoop() ) { + throw new BoxValidationException( "You cannot use the 'loop' method of the exit component outside of a custom tag." ); + } + // Swallowing aborts here if type="page" and exits of type template // Ignoring showerror in case for now if ( e.isRequest() ) { + context.flushBuffer( true ); throw e; } } catch ( Throwable e ) { diff --git a/src/main/java/ortus/boxlang/runtime/types/Function.java b/src/main/java/ortus/boxlang/runtime/types/Function.java index 6ada28eed..fe12823b3 100644 --- a/src/main/java/ortus/boxlang/runtime/types/Function.java +++ b/src/main/java/ortus/boxlang/runtime/types/Function.java @@ -38,7 +38,9 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.services.InterceptorService; +import ortus.boxlang.runtime.types.exceptions.AbortException; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; import ortus.boxlang.runtime.types.meta.BoxMeta; import ortus.boxlang.runtime.types.meta.FunctionMeta; import ortus.boxlang.runtime.util.ArgumentUtil; @@ -178,6 +180,16 @@ public Object invoke( FunctionBoxContext context ) { BoxEvent.POST_FUNCTION_INVOKE, data ); + } catch ( AbortException e ) { + if ( e.isLoop() ) { + throw new BoxValidationException( "You cannot use the 'loop' method of the exit component outside of a custom tag." ); + } else if ( e.isTemplate() || e.isTag() ) { + // These function basically as a return from the method + return result; + } else if ( e.isRequest() ) { + context.flushBuffer( true ); + } + throw e; } catch ( Throwable e ) { context.flushBuffer( true ); throw e; diff --git a/src/main/java/ortus/boxlang/runtime/types/exceptions/AbortException.java b/src/main/java/ortus/boxlang/runtime/types/exceptions/AbortException.java index 81e5207e0..fb9e9121f 100644 --- a/src/main/java/ortus/boxlang/runtime/types/exceptions/AbortException.java +++ b/src/main/java/ortus/boxlang/runtime/types/exceptions/AbortException.java @@ -61,4 +61,32 @@ public Boolean isPage() { return type.equals( "page" ); } + // other types: exit-tag, exit-template, exit-loop + + /** + * Is this abort type tag? Use with exit component. + * + * @return Whether this abort affects the tag + */ + public Boolean isTag() { + return type.equals( "exit-tag" ); + } + + /** + * Is this abort type template? Use with exit component. + * + * @return Whether this abort affects the template + */ + public Boolean isTemplate() { + return type.equals( "exit-template" ); + } + + /** + * Is this abort type loop? Use with exit component. + * + * @return Whether this abort affects the loop + */ + public Boolean isLoop() { + return type.equals( "exit-loop" ); + } } diff --git a/src/test/java/ortus/boxlang/runtime/components/system/ExitTest.java b/src/test/java/ortus/boxlang/runtime/components/system/ExitTest.java new file mode 100644 index 000000000..e143b1c05 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/components/system/ExitTest.java @@ -0,0 +1,322 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * 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 ortus.boxlang.runtime.components.system; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; + +public class ExitTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + } + + @DisplayName( "It can exit" ) + @Test + public void testCanExitTag() { + + instance.executeSource( + """ + + + + """, + context, BoxSourceType.CFTEMPLATE ); + assertThat( variables.getAsString( result ) ).contains( "before" ); + } + + @DisplayName( "It can exit" ) + @Test + public void testCanExitBLTag() { + + instance.executeSource( + """ + + + + """, + context, BoxSourceType.BOXTEMPLATE ); + assertThat( variables.getAsString( result ) ).contains( "before" ); + } + + @DisplayName( "It can exit script" ) + @Test + public void testCanExitScript() { + + instance.executeSource( + """ + result = "before" + exit; + result = "after" + """, + context ); + assertThat( variables.getAsString( result ) ).contains( "before" ); + } + + @DisplayName( "It can exit ACF script" ) + @Test + public void testCanExitACFScript() { + + instance.executeSource( + """ + result = "before" + cfexit(); + result = "after" + """, + context, BoxSourceType.CFSCRIPT ); + assertThat( variables.getAsString( result ) ).contains( "before" ); + } + + @DisplayName( "It cannot catch exit" ) + @Test + public void testCannotCatchExit() { + + instance.executeSource( + """ + + + + + + + + + """, + context, BoxSourceType.CFTEMPLATE ); + + assertThat( variables.getAsString( result ) ).contains( "before" ); + } + + @Test + public void testCanExitInclude() { + + instance.executeSource( + """ + request.exitMethod="exitTag" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm"; + result &= "afterinclude"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforeafterinclude" ); + + instance.executeSource( + """ + request.exitMethod="exitTemplate" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm"; + result &= "afterinclude"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforeafterinclude" ); + + assertThrows( BoxValidationException.class, () -> instance.executeSource( + """ + request.exitMethod="loop" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm"; + """, + context ) ); + } + + @Test + public void testCanExitModuleStart() { + + instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitWhen="start" + request.exitMethod="exitTag" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + result &= "aftermodule"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforestartaftermodule" ); + + instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitMethod="exitTemplate" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + result &= "aftermodule"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforestartbodyendaftermodule" ); + + assertThrows( BoxValidationException.class, () -> instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitMethod="loop" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + """, + context ) ); + } + + @Test + public void testCanExitModuleEnd() { + + instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitWhen="end" + request.exitMethod="exitTag" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + result &= "aftermodule"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "startbodybeforeendaftermodule" ); + + instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitMethod="exitTemplate" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + result &= "aftermodule"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "startbodybeforeendaftermodule" ); + + instance.executeSource( + """ + request.loopCount=0; + result = ""; + request.exitMethod="loop" + module template="src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm" { + result &= "body"; + } + result &= "aftermodule"; + } + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "startbodybeforeendbodyendaftermodule" ); + } + + @Test + public void testCanExitUDF() { + + instance.executeSource( + """ + result = ""; + request.exitMethod="exitTag" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm"; + result &= "afterinclude"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforebeforeUDFafterafterinclude" ); + + instance.executeSource( + """ + result = ""; + request.exitMethod="exitTemplate" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm"; + result &= "afterinclude"; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforebeforeUDFafterafterinclude" ); + + assertThrows( BoxValidationException.class, () -> instance.executeSource( + """ + result = ""; + request.exitMethod="loop" + include "src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm"; + """, + context ) ); + } + + @Test + public void testCanExitClassMethod() { + + instance.executeSource( + """ + request.result = ""; + request.exitMethod="exitTag" + new src.test.java.ortus.boxlang.runtime.components.system.ExitTests.ExitClass(); + request.result &= "aftermethod"; + result = request.result; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforebeforeUDFafteraftermethod" ); + + instance.executeSource( + """ + request.result = ""; + request.exitMethod="exitTemplate" + new src.test.java.ortus.boxlang.runtime.components.system.ExitTests.ExitClass(); + request.result &= "aftermethod"; + result = request.result; + """, + context ); + assertThat( variables.getAsString( result ) ).isEqualTo( "beforebeforeUDFafteraftermethod" ); + + assertThrows( BoxValidationException.class, () -> instance.executeSource( + """ + request.result = ""; + request.exitMethod="loop" + new src.test.java.ortus.boxlang.runtime.components.system.ExitTests.ExitClass(); + request.result &= "aftermethod"; + result = request.result; + """, + context ) ); + } + +} diff --git a/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/ExitClass.bx b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/ExitClass.bx new file mode 100644 index 000000000..fa8cee7dd --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/ExitClass.bx @@ -0,0 +1,15 @@ +class { + + function init() { + request.result &= "before" + foo() + request.result &= "after" + } + + function foo() { + request.result &= "beforeUDF" + exit method="#request.exitMethod#"; + request.result &= "afterUDF" + } + +} \ No newline at end of file diff --git a/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm new file mode 100644 index 000000000..f374ea945 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/UDFInclude.bxm @@ -0,0 +1,11 @@ + + function foo() { + result &= "beforeUDF" + exit method="#request.exitMethod#"; + result &= "afterUDF" + } + + + + + \ No newline at end of file diff --git a/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm new file mode 100644 index 000000000..545da750f --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/module.bxm @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm new file mode 100644 index 000000000..eab075eb5 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/components/system/ExitTests/plainInclude.bxm @@ -0,0 +1,3 @@ + + + \ No newline at end of file