This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-5256-freemarker-compress in repository https://gitbox.apache.org/repos/asf/struts.git
commit f3fe49f60a6f2e739c5b959229f4d0e904233d83 Author: Lukasz Lenart <[email protected]> AuthorDate: Sun Jul 14 09:23:34 2024 +0200 WW-5256 Implements dedicated tag to compress output --- .../org/apache/struts2/components/Compress.java | 102 ++++++++++++++++ .../org/apache/struts2/views/jsp/CompressTag.java | 55 +++++++++ .../resources/template/css_xhtml/controlfooter.ftl | 17 ++- .../resources/template/css_xhtml/form-validate.ftl | 10 +- .../main/resources/template/css_xhtml/label.ftl | 1 - .../main/resources/template/simple/actionerror.ftl | 14 +-- .../template/simple/form-close-tooltips.ftl | 1 - .../main/resources/template/simple/form-close.ftl | 2 - .../resources/template/xhtml/controlheader.ftl | 12 +- .../resources/template/xhtml/form-validate.ftl | 12 +- .../site/resources/tags/compress-attributes.html | 32 +++++ .../site/resources/tags/compress-description.html | 1 + .../apache/struts2/components/CompressTest.java | 132 +++++++++++++++++++++ .../struts2/views/jsp/AbstractUITagTest.java | 15 +++ .../apache/struts2/views/jsp/CompressTagTest.java | 73 ++++++++++++ .../apache/struts2/views/jsp/ui/DebugTagTest.java | 16 --- 16 files changed, 441 insertions(+), 54 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/components/Compress.java b/core/src/main/java/org/apache/struts2/components/Compress.java new file mode 100644 index 000000000..6131e5f33 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/components/Compress.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.components; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.util.ValueStack; +import org.apache.struts2.views.annotations.StrutsTag; +import org.apache.struts2.views.annotations.StrutsTagAttribute; + +import java.io.Writer; + +/** + * <p> + * Used to compress HTML output. Just wrap a given section with the tag. + * </p> + * + * <p> + * Configurable attributes are: + * </p> + * + * <ul> + * <li>force (true/false) - always compress output, this can be useful in DevMode as devMode disables compression</li> + * </ul> + * + * <p><b>Examples</b></p> + * <pre> + * <!-- START SNIPPET: example --> + * <s:compress> + * <s:form action="submit"> + * <s:text name="name" /> + * ... + * </s:form> + * </s:compress> + * <!-- END SNIPPET: example --> + * </pre> + * + * <p>Uses conditional compression depending on action</p> + * <pre> + * <!-- START SNIPPET: example --> + * <s:compress force="shouldCompress"> + * <s:form action="submit"> + * <s:text name="name" /> + * ... + * </s:form> + * </s:compress> + * <!-- END SNIPPET: example --> + * </pre> + * "shouldCompress" is a field with getter define on action used in expression evaluation + */ +@StrutsTag(name = "compress", tldTagClass = "org.apache.struts2.views.jsp.CompressTag", + description = "Compress wrapped content") +public class Compress extends Component { + + private static final Logger LOG = LogManager.getLogger(Compress.class); + + private String force; + + public Compress(ValueStack stack) { + super(stack); + } + + @Override + public boolean end(Writer writer, String body) { + Object forceValue = findValue(force, Boolean.class); + boolean forced = forceValue != null && Boolean.parseBoolean(forceValue.toString()); + if (devMode && !forced) { + LOG.debug("Avoids compressing output: {} in DevMode", body); + return super.end(writer, body, true); + } + LOG.trace("Compresses: {}", body); + String compressed = body.trim().replaceAll(">\\s+<", "><"); + LOG.trace("Compressed: {}", compressed); + return super.end(writer, compressed, true); + } + + @Override + public boolean usesBody() { + return true; + } + + @StrutsTagAttribute(description = "Force output compression") + public void setForce(String force) { + this.force = force; + } +} diff --git a/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java b/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java new file mode 100644 index 000000000..4b9821e53 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.views.jsp; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.struts2.components.Component; +import org.apache.struts2.components.Compress; +import org.apache.struts2.util.ValueStack; + +import java.io.Serial; + +/** + * @see org.apache.struts2.components.Compress + */ +public class CompressTag extends ComponentTagSupport { + + @Serial + private static final long serialVersionUID = 7572566991679717145L; + + private String force; + + @Override + public Component getBean(ValueStack stack, HttpServletRequest req, HttpServletResponse res) { + return new Compress(stack); + } + + @Override + protected void populateParams() { + super.populateParams(); + + Compress compress = (Compress) component; + compress.setForce(force); + } + + public void setForce(String force) { + this.force = force; + } +} diff --git a/core/src/main/resources/template/css_xhtml/controlfooter.ftl b/core/src/main/resources/template/css_xhtml/controlfooter.ftl index d25b5790c..0099b6df8 100644 --- a/core/src/main/resources/template/css_xhtml/controlfooter.ftl +++ b/core/src/main/resources/template/css_xhtml/controlfooter.ftl @@ -19,29 +19,26 @@ */ --> ${attributes.after!}<#t/> - <#lt/> <#if !attributes.labelPosition?? && (attributes.form.labelPosition)??> <#assign labelPos = attributes.form.labelPosition/> <#elseif attributes.labelPosition??> <#assign labelPos = attributes.labelPosition/> </#if> <#if (labelPos!"top") == 'top'> -</div> <#rt/> +</div><#rt/> <#else> -</span> <#rt/> +</span><#rt/> </#if> <#if (attributes.errorposition!"top") == 'bottom'> <#assign hasFieldErrors = attributes.name?? && fieldErrors?? && fieldErrors.get(attributes.name)??/> <#if hasFieldErrors> <div <#rt/><#if attributes.id??>id="wwerr_${attributes.id}"<#rt/></#if> class="wwerr"> <#list fieldErrors.get(attributes.name) as error> - <div<#rt/> - <#if attributes.id??> - errorFor="${attributes.id}"<#rt/> - </#if> - class="errorMessage"> - ${error} - </div><#t/> +<div<#rt/> +<#if attributes.id??> + errorFor="${attributes.id}"<#rt/> +</#if> + class="errorMessage">${error}</div><#rt/> </#list> </div><#t/> </#if> diff --git a/core/src/main/resources/template/css_xhtml/form-validate.ftl b/core/src/main/resources/template/css_xhtml/form-validate.ftl index 7beb7fab5..1f1bff261 100644 --- a/core/src/main/resources/template/css_xhtml/form-validate.ftl +++ b/core/src/main/resources/template/css_xhtml/form-validate.ftl @@ -20,9 +20,9 @@ --> <#if attributes.validate!false == true> <@s.script src="${base}${attributes.staticContentPath}/css_xhtml/validation.js"/> - <#if attributes.onsubmit??> - ${tag.addParameter('onsubmit', "${attributes.onsubmit}; return validateForm_${attributes.escapedId}();")} - <#else> - ${tag.addParameter('onsubmit', "return validateForm_${attributes.escapedId}();")} - </#if> +<#if attributes.onsubmit??> + ${tag.addParameter('onsubmit', "${attributes.onsubmit}; return validateForm_${attributes.escapedId}();")} +<#else> + ${tag.addParameter('onsubmit', "return validateForm_${attributes.escapedId}();")} +</#if> </#if> diff --git a/core/src/main/resources/template/css_xhtml/label.ftl b/core/src/main/resources/template/css_xhtml/label.ftl index 23701e715..4c03cd19a 100644 --- a/core/src/main/resources/template/css_xhtml/label.ftl +++ b/core/src/main/resources/template/css_xhtml/label.ftl @@ -18,7 +18,6 @@ * under the License. */ --> -<#--include "/${attributes.templateDir}/css_xhtml/controlheader.ftl" /--> <#include "/${attributes.templateDir}/${attributes.expandTheme}/controlheader.ftl" /> <label<#rt/> <#if attributes.id??> diff --git a/core/src/main/resources/template/simple/actionerror.ftl b/core/src/main/resources/template/simple/actionerror.ftl index 4cd9a60b9..54b3f3ca2 100644 --- a/core/src/main/resources/template/simple/actionerror.ftl +++ b/core/src/main/resources/template/simple/actionerror.ftl @@ -19,7 +19,7 @@ */ --> <#if (actionErrors?? && actionErrors?size > 0)> - <ul<#rt/> +<ul<#rt/> <#if attributes.id??> id="${attributes.id}"<#rt/> </#if> @@ -32,10 +32,10 @@ style="${attributes.cssStyle}"<#rt/> </#if> > - <#list actionErrors as error> - <#if error??> - <li><span><#if attributes.escape>${error!}<#else>${error!?no_esc}</#if></span><#rt/></li><#rt/> - </#if> - </#list> - </ul> +<#list actionErrors as error> +<#if error??> + <li><span><#if attributes.escape>${error!}<#else>${error!?no_esc}</#if></span><#rt/></li><#rt/> +</#if> +</#list> +</ul> </#if> \ No newline at end of file diff --git a/core/src/main/resources/template/simple/form-close-tooltips.ftl b/core/src/main/resources/template/simple/form-close-tooltips.ftl index 1bf45e909..e57bdcc1e 100644 --- a/core/src/main/resources/template/simple/form-close-tooltips.ftl +++ b/core/src/main/resources/template/simple/form-close-tooltips.ftl @@ -18,7 +18,6 @@ * under the License. */ --> - <#-- Code that will add javascript needed for tooltips --><#t/> diff --git a/core/src/main/resources/template/simple/form-close.ftl b/core/src/main/resources/template/simple/form-close.ftl index 59d4f0dbc..291af0977 100644 --- a/core/src/main/resources/template/simple/form-close.ftl +++ b/core/src/main/resources/template/simple/form-close.ftl @@ -19,7 +19,6 @@ */ --> </form> - <#if (attributes.customOnsubmitEnabled??)> <@s.script> <#-- @@ -98,5 +97,4 @@ </#if> </@s.script> </#if> - <#include "/${attributes.templateDir}/${attributes.expandTheme}/form-close-tooltips.ftl" /> diff --git a/core/src/main/resources/template/xhtml/controlheader.ftl b/core/src/main/resources/template/xhtml/controlheader.ftl index ec9372d92..2df6b1cb0 100644 --- a/core/src/main/resources/template/xhtml/controlheader.ftl +++ b/core/src/main/resources/template/xhtml/controlheader.ftl @@ -19,10 +19,10 @@ */ --> <#include "/${attributes.templateDir}/${attributes.expandTheme}/controlheader-core.ftl" /> - <td - <#if attributes.align?? > - class="align-${attributes.align}" - <#else > - class="tdInput" - </#if> + <td<#rt/> +<#if attributes.align?? > + class="align-${attributes.align}"<#rt/> +<#else > + class="tdInput"<#rt/> +</#if> ><#t/> diff --git a/core/src/main/resources/template/xhtml/form-validate.ftl b/core/src/main/resources/template/xhtml/form-validate.ftl index a074bef2f..cbd03a7f6 100644 --- a/core/src/main/resources/template/xhtml/form-validate.ftl +++ b/core/src/main/resources/template/xhtml/form-validate.ftl @@ -19,10 +19,10 @@ */ --> <#if attributes.validate!false == true> - <@s.script src="${base}${attributes.staticContentPath}/xhtml/validation.js" /> - <#if attributes.onsubmit??> - ${tag.addParameter('onsubmit', "${attributes.onsubmit}; return validateForm_${attributes.escapedId}();")} - <#else> - ${tag.addParameter('onsubmit', "return validateForm_${attributes.escapedId}();")} - </#if> +<@s.script src="${base}${attributes.staticContentPath}/xhtml/validation.js" /> +<#if attributes.onsubmit??> + ${tag.addParameter('onsubmit', "${attributes.onsubmit}; return validateForm_${attributes.escapedId}();")} +<#else> + ${tag.addParameter('onsubmit', "return validateForm_${attributes.escapedId}();")} +</#if> </#if> diff --git a/core/src/site/resources/tags/compress-attributes.html b/core/src/site/resources/tags/compress-attributes.html new file mode 100644 index 000000000..9ecf24227 --- /dev/null +++ b/core/src/site/resources/tags/compress-attributes.html @@ -0,0 +1,32 @@ +<table class="tag-reference"> + <tr> + <td colspan="6"><h4>Dynamic Attributes Allowed:</h4> false</td> + </tr> + <tr> + <td colspan="6"><hr/></td> + </tr> + <tr> + <th class="tag-header"><h4>Name</h4></th> + <th class="tag-header"><h4>Required</h4></th> + <th class="tag-header"><h4>Default</h4></th> + <th class="tag-header"><h4>Evaluated</h4></th> + <th class="tag-header"><h4>Type</h4></th> + <th class="tag-header"><h4>Description</h4></th> + </tr> + <tr> + <td class="tag-attribute">force</td> + <td class="tag-attribute">false</td> + <td class="tag-attribute"></td> + <td class="tag-attribute">false</td> + <td class="tag-attribute">String</td> + <td class="tag-attribute">Force output compression</td> + </tr> + <tr> + <td class="tag-attribute">performClearTagStateForTagPoolingServers</td> + <td class="tag-attribute">false</td> + <td class="tag-attribute">false</td> + <td class="tag-attribute">false</td> + <td class="tag-attribute">Boolean</td> + <td class="tag-attribute">Whether to clear all tag state during doEndTag() processing (if applicable)</td> + </tr> +</table> diff --git a/core/src/site/resources/tags/compress-description.html b/core/src/site/resources/tags/compress-description.html new file mode 100644 index 000000000..6865f0b72 --- /dev/null +++ b/core/src/site/resources/tags/compress-description.html @@ -0,0 +1 @@ +Compress wrapped content diff --git a/core/src/test/java/org/apache/struts2/components/CompressTest.java b/core/src/test/java/org/apache/struts2/components/CompressTest.java new file mode 100644 index 000000000..6631850ab --- /dev/null +++ b/core/src/test/java/org/apache/struts2/components/CompressTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.components; + +import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.util.ValueStack; +import org.apache.struts2.util.ValueStackFactory; + +import java.io.StringWriter; +import java.util.Map; + +public class CompressTest extends StrutsInternalTestCase { + + private ValueStack stack; + private Map<String, Object> context; + + public void testCompressHtmlOutput() { + Compress compress = new Compress(stack); + + String body = """ + <html> + <head> + <title>File upload: result</title> + </head> + <body> + <h1>File upload: result</h1> + </body> + </html> + """; + + StringWriter writer = new StringWriter(); + + compress.setDevMode("false"); + compress.setForce("false"); + compress.end(writer, body); + + assertEquals("<html><head><title>File upload: result</title></head><body><h1>File upload: result</h1></body></html>", writer.toString()); + } + + public void testAvoidCompressingInDevModeHtmlOutput() { + Compress compress = new Compress(stack); + + String body = """ + <html> + <head> + <title>File upload: result</title> + </head> + <body> + <h1>File upload: result</h1> + </body> + </html> + """; + + StringWriter writer = new StringWriter(); + + compress.setDevMode("true"); + compress.end(writer, body); + + assertEquals(body, writer.toString()); + } + + public void testCompressHtmlOutputEvenInDevMode() { + Compress compress = new Compress(stack); + + String body = """ + <html> + <head> + <title>File upload: result</title> + </head> + <body> + <h1>File upload: result</h1> + </body> + </html> + """; + + StringWriter writer = new StringWriter(); + + compress.setDevMode("true"); + compress.setForce("true"); + compress.end(writer, body); + + assertEquals("<html><head><title>File upload: result</title></head><body><h1>File upload: result</h1></body></html>", writer.toString()); + } + + public void testCompressHtmlOutputEvenInDevModeAndForceIsExpression() { + Compress compress = new Compress(stack); + + String body = """ + <html> + <head> + <title>File upload: result</title> + </head> + <body> + <h1>File upload: result</h1> + </body> + </html> + """; + + this.context.put("shouldCompress", Boolean.TRUE); + + compress.setDevMode("true"); + compress.setForce("shouldCompress"); + + StringWriter writer = new StringWriter(); + compress.end(writer, body); + + assertEquals("<html><head><title>File upload: result</title></head><body><h1>File upload: result</h1></body></html>", writer.toString()); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + stack = container.getInstance(ValueStackFactory.class).createValueStack(); + context = stack.getContext(); + } +} diff --git a/core/src/test/java/org/apache/struts2/views/jsp/AbstractUITagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/AbstractUITagTest.java index df1e4323d..6252c4554 100644 --- a/core/src/test/java/org/apache/struts2/views/jsp/AbstractUITagTest.java +++ b/core/src/test/java/org/apache/struts2/views/jsp/AbstractUITagTest.java @@ -23,6 +23,7 @@ import org.apache.commons.beanutils.BeanUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsConstants; import org.apache.struts2.dispatcher.mapper.ActionMapper; import org.apache.struts2.dispatcher.mapper.DefaultActionMapper; import org.apache.struts2.views.jsp.ui.AbstractUITag; @@ -295,4 +296,18 @@ public abstract class AbstractUITagTest extends AbstractTagTest { return buffer.toString(); } + + protected void setDevMode(final boolean devMode) { + setStrutsConstant(Collections.singletonMap(StrutsConstants.STRUTS_DEVMODE, Boolean.toString(devMode))); + } + + /** + * Overwrite the Struts Constant and reload container + */ + @Override + protected void setStrutsConstant(final Map<String, String> overwritePropeties) { + super.setStrutsConstant(overwritePropeties); + stack.getActionContext().withContainer(container); + } + } diff --git a/core/src/test/java/org/apache/struts2/views/jsp/CompressTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/CompressTagTest.java new file mode 100644 index 000000000..994e948af --- /dev/null +++ b/core/src/test/java/org/apache/struts2/views/jsp/CompressTagTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.views.jsp; + +import org.apache.struts2.views.jsp.ui.StrutsBodyContent; + +public class CompressTagTest extends AbstractUITagTest { + + public void testNoCompression() throws Exception { + setDevMode(true); + + CompressTag tag = new CompressTag(); + tag.setPageContext(pageContext); + + StrutsBodyContent bc = new StrutsBodyContent(null); + bc.print(""" + <form action="/" method="post"> + <table class="wwFormTable"></table></form> + """ + ); + + tag.doStartTag(); + tag.setBodyContent(bc); + tag.doEndTag(); + + assertEquals(""" + <form action="/" method="post"> + <table class="wwFormTable"></table></form> + """.stripTrailing(), + this.writer.toString()); + } + + public void testForceCompression() throws Exception { + setDevMode(true); + + CompressTag tag = new CompressTag(); + tag.setPageContext(pageContext); + tag.setForce("true"); + + StrutsBodyContent bc = new StrutsBodyContent(null); + bc.print(""" + <form action="/" method="post"> + <table class="wwFormTable"></table></form> + """ + ); + + tag.doStartTag(); + tag.setBodyContent(bc); + tag.doEndTag(); + + assertEquals(""" + <form action="/" method="post"><table class="wwFormTable"></table></form> + """.stripTrailing(), + this.writer.toString()); + } + +} \ No newline at end of file diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java index d1e89a099..ffc706e0f 100644 --- a/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java +++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/DebugTagTest.java @@ -19,13 +19,9 @@ package org.apache.struts2.views.jsp.ui; import org.apache.commons.lang3.StringUtils; -import org.apache.struts2.StrutsConstants; import org.apache.struts2.dispatcher.PrepareOperations; import org.apache.struts2.views.jsp.AbstractUITagTest; -import java.util.Collections; -import java.util.Map; - /** * Test case for {@link org.apache.struts2.components.Debug}. */ @@ -203,16 +199,4 @@ public class DebugTagTest extends AbstractUITagTest { PrepareOperations.clearDevModeOverride(); // Clear DevMode override. Avoid ThreadLocal side-effects if test thread re-used. } - private void setDevMode(final boolean devMode) { - setStrutsConstant(Collections.singletonMap(StrutsConstants.STRUTS_DEVMODE, Boolean.toString(devMode))); - } - - /** - * Overwrite the Struts Constant and reload container - */ - @Override - protected void setStrutsConstant(final Map<String, String> overwritePropeties) { - super.setStrutsConstant(overwritePropeties); - stack.getActionContext().withContainer(container); - } }
