Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WICKET-6823: Add a container to AbstractTransformerBehavior to support multiple instances per component. #457

Open
wants to merge 6 commits into
base: wicket-8.x
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
*/
package org.apache.wicket.markup.transformer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import org.apache.wicket.Component;
import org.apache.wicket.Page;
import org.apache.wicket.WicketRuntimeException;
Expand All @@ -26,17 +31,114 @@
import org.apache.wicket.request.http.WebResponse;

/**
* A {@link Behavior} which can be added to any component. It allows to post-process (transform) the
* A {@link Behavior} which can be added to any component, allowing to post-process (transform) the
* markup generated by the component.
*
* <p>
* There's one important limitation with the current implementation: Multiple different instances of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Javadoc is obsolete with the improvement below, no ?

Copy link
Contributor Author

@ams-tschoening ams-tschoening Oct 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, AbstractTransformerBehavior itself keeps the limitation and especially existing users should be made aware. The container is only one possible workaround/solution, a different implementation of AbstractTransformerBehavior itself would be another one. With that the sentence could be removed, but with the container only the sentence still applies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just say "If you need to apply several transformations on the same component then use {@linkplain AbstractTransformerBehavior.Multi}".
I see no need to write 20 lines as Javadoc explaining history.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those 20 lines of history are exactly what I regularly miss when running into trouble with Wicket, because without them there's no basic understanding of why things are the way they are. When running into this problem, I needed to look at the entire GIT-history just to find no reason for the current implementation, no clue on how things were supposed to work, for a workaround fitting the existing design etc.

The docs often lack important context from my experience, so I'm trying to add one here.

* this behavior CAN NOT be assigned to the same component! If one wishes to do so, the contained
* container needs to be used to wrap existing behaviors and that container needs to be added to the
* component instead. The current implementation works with temporary responses, but doesn't support
* nesting itself properly, which results in missing rendered output and most likely broken HTML
* documents in the end.
* </p>
* @see org.apache.wicket.markup.transformer.AbstractOutputTransformerContainer
*
*
* @author Juergen Donnerstag
*/
public abstract class AbstractTransformerBehavior extends Behavior implements ITransformer
{
private static final long serialVersionUID = 1L;

/**
* Container to apply multiple {@link AbstractTransformerBehavior} to some component.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This javadoc should really be a comment in the PR, not Javadoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As someone who ran into that problem, I prefer having such details in the file to get a basic understanding of problems like these without looking at source history, JIRA etc. I regularly miss such details from Wicket's docs and rephrased things to not sound like a commit message too much anymore.

* <p>
* This container is by design NOT about multiple arbitrary transformations, but really only for
* the one use case supporting multiple instances of {@link AbstractTransformerBehavior} on one
* and the same component. The current implementation of that makes use of temporary responses,
* but doesn't support nesting itself properly if multiple behaviors get assigned to the same
* component. That results in missing rendered output and most likely entirely broken HTML.
* </p>
* <p>
* The easiest workaround for that problem is using this container in those cases: An instance
* needs to be created with all transformers of interest in the order they should be applied and
* the container takes care of doing so. Being an {@link AbstractTransformerBehavior} itself,
* things simply work like with individual behaviors, while response handling is managed by the
* container only. This means that callbacks of the internally maintained instances, like
* {@link AbstractTransformerBehavior#afterRender(Component)} etc., are NOT used anymore! OTOH,
* this way the behaviors stay individually useful without the container as well.
* </p>
* @see <a href="https://issues.apache.org/jira/projects/WICKET/issues/WICKET-6823">JIRA issue</a>
*/
public static class Multi extends AbstractTransformerBehavior
{
private static final long serialVersionUID = 1L;

/**
* All transformers which need to be applied in the order given by the user, which is the
* same order as processed by the container in the end.
*/
private final List<AbstractTransformerBehavior> transformers;

/**
* Constructor simply storing the given transformers.
*
* @param transformers, which must not be {@code null} or empty. Wouldn't makes sense here.
*/
private Multi(List<AbstractTransformerBehavior> transformers)
{
if ((transformers == null) || transformers.isEmpty())
{
throw new IllegalArgumentException("No transformers given.");
}

this.transformers = transformers;
}

@Override
public CharSequence transform(Component component, CharSequence output) throws Exception
{
CharSequence retVal = output;
for (AbstractTransformerBehavior trans : this.transformers)
{
retVal = trans.transform(component, retVal);
}

return retVal;
}

/**
* Create a new container with the given transformers and with keeping their order.
* <p>
* This factory expects multiple individual transformers already, as creating a container
* with less of those doesn't make too much sense and users should reconsider then if this
* container is of use at all. In most cases users do have individual transformers to apply
* only anyway and don't need to provide a collection themself this way. OTOH, a collection
* could be empty, contain only one element etc. and would then defeat the purpose of this
* container again.
* </p>
* @param first First transformer to apply.
* @param second Second transformer to apply.
* @param moreIf All other transformers to apply, if at all, in given order.
* @return A container with multiple transformers being applied.
*/
public static Multi of( AbstractTransformerBehavior first,
AbstractTransformerBehavior second,
AbstractTransformerBehavior... moreIf)
{
List<AbstractTransformerBehavior> transformers = new ArrayList<>();

transformers.add(Objects.requireNonNull(first, "No first transformer given."));
transformers.add(Objects.requireNonNull(second, "No second transformer given."));

if ((moreIf != null) && (moreIf.length > 0))
{
transformers.addAll(Arrays.asList(moreIf));
}

return new Multi(transformers);
}
}

/**
* The request cycle's response before the transformation.
*/
Expand All @@ -45,10 +147,10 @@ public abstract class AbstractTransformerBehavior extends Behavior implements IT
/**
* Create a new response object which is used to store the markup generated by the child
* objects.
*
*
* @param originalResponse
* the original web response or {@code null} if it isn't a {@link WebResponse}
*
*
* @return Response object. Must not be null
*/
protected BufferedWebResponse newResponse(final WebResponse originalResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
package org.apache.wicket.markup.transformer;

import java.util.function.Function;

import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
Expand All @@ -37,7 +39,10 @@ public class AbstractTransformerBehaviorTest extends WicketTestCase
@Test
public void responseTransformation()
{
TestPage testPage = new TestPage();
TestPage testPage = new TestPage();
String replacement = "replacement";

testPage.add(new Label(TestPage.LABEL_ID, TestPage.LABEL_VAL));
testPage.add(new AbstractTransformerBehavior()
{
/** */
Expand All @@ -47,11 +52,12 @@ public void responseTransformation()
public CharSequence transform(Component component, CharSequence output)
throws Exception
{
return output.toString().replace("to be replaced", "replacement");
return output.toString().replace(TestPage.LABEL_VAL, replacement);
}
});

tester.startPage(testPage);
assertTrue(tester.getLastResponseAsString().contains("replacement"));
assertTrue(tester.getLastResponseAsString().contains(replacement));
}

/**
Expand All @@ -69,7 +75,77 @@ public void transformationInAjaxRequest()
tester.clickLink("updateLabel", true);
tester.assertContains("ajax request");
tester.assertContainsNot("normal request");
}

/**
* Test how multiple different transformers applied to the same component behave.
* <p>
* The current implementation of {@link AbstractTransformerBehavior} doesn't support multiple
* instances on the same component, a container needs to be used explicitly instead. So make
* sure the implementation is as expected, as otherwise the container might not be necessary at
* all anymore, and that the container really works around the problem.
* </p>
* @see <a href="https://issues.apache.org/jira/projects/WICKET/issues/WICKET-6823">JIRA issue</a>
*/
@Test
public void multiTransesSameComp()
{
TestPage testPage = new TestPage();
Label label = new Label(TestPage.LABEL_ID, TestPage.LABEL_VAL);

Function<String, AbstractTransformerBehavior> transformerProducer = (val) ->
{
return new AbstractTransformerBehavior()
{
/** */
private static final long serialVersionUID = 1L;

@Override
public CharSequence transform(Component component, CharSequence output)
throws Exception
{
String outStr = output.toString();
if (outStr.contains(TestPage.LABEL_VAL))
{
return outStr.replace(TestPage.LABEL_VAL, val);
}

// Make somewhat sure to recognize that BOTH transformers have been applied.
return outStr.replaceAll
(
">(.+)</span>",
String.format(">$1%s</span>", val)
);
}
};
};

label.add(transformerProducer.apply("foo"));
label.add(transformerProducer.apply("bar"));

// Make sure the expected limited implementation is still available, which makes a container
// necessary only at all. If that has changed, the container might be removed as well.
testPage.add(label);
tester.startPage(testPage);
tester.isVisible(TestPage.LABEL_ID);
tester.assertContains(">foo</span>");
tester.assertContainsNot("</html>");

testPage = new TestPage();
label = new Label(TestPage.LABEL_ID, TestPage.LABEL_VAL);

label.add(AbstractTransformerBehavior.Multi.of
(
transformerProducer.apply("foo"),
transformerProducer.apply("bar")
));

// Maike sure that the container provided as workaround really fixes the problem.
testPage.add(label);
tester.startPage(testPage);
tester.isVisible(TestPage.LABEL_ID);
tester.assertContains(">foobar</span>");
tester.assertContains("</html>");
}

private static class AjaxTestPage extends WebPage implements IMarkupResourceStreamProvider
Expand Down Expand Up @@ -126,12 +202,14 @@ private static class TestPage extends WebPage implements IMarkupResourceStreamPr
{
private static final long serialVersionUID = 1L;

private static final String LABEL_ID = "label";
private static final String LABEL_VAL = "{to be replaced}";

@Override
public IResourceStream getMarkupResourceStream(MarkupContainer container,
Class<?> containerClass)
{
return new StringResourceStream("<html><body>{to be replaced}</body></html>");
return new StringResourceStream("<html><body><span wicket:id='label'></span></body></html>");
}

}
}