melloware / quarkus-faces

Quarkus meets Faces
https://quarkus-faces-melloware-8a6a34c1.koyeb.app/
MIT License
75 stars 23 forks source link

Programmatically creating facelets with @View doesn't work #444

Open simasch opened 4 months ago

simasch commented 4 months ago

I tried to add a programmatic view with the @View annotation, but when I access http://localhost:8080/facelet.xhml it returns 404

@View("/facelet.xhtml")
@ApplicationScoped
public class FaceletView extends Facelet {

    @Override
    public void apply(FacesContext facesContext, UIComponent parent) {
        if (!facesContext.getAttributes().containsKey(IS_BUILDING_INITIAL_STATE)) {
            return;
        }

        var components = new ComponentBuilder(facesContext);
        var rootChildren = parent.getChildren();

        var doctype = new UIOutput();
        doctype.setValue("<!DOCTYPE html>");
        rootChildren.add(doctype);

        var htmlTag = new UIOutput();
        htmlTag.setValue("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
        rootChildren.add(htmlTag);

        HtmlBody body = components.create(HtmlBody.COMPONENT_TYPE);
        rootChildren.add(body);

        HtmlForm form = components.create(HtmlForm.COMPONENT_TYPE);
        form.setId("form");
        body.getChildren().add(form);

        HtmlOutputText message = components.create(HtmlOutputText.COMPONENT_TYPE);
        message.setId("message");

        HtmlCommandButton actionButton = components.create(HtmlCommandButton.COMPONENT_TYPE);
        actionButton.setId("button");
        actionButton.addActionListener(
                e -> message.setValue("Hello, World! Welcome to Faces 4.0 on Jakarta EE 10"));
        actionButton.setValue("Greet");

        form.getChildren().add(actionButton);

        parent.getChildren().add(message);

        htmlTag = new UIOutput();
        htmlTag.setValue("</html>");
        rootChildren.add(htmlTag);
    }

    private static class ComponentBuilder {
        FacesContext facesContext;

        ComponentBuilder(FacesContext facesContext) {
            this.facesContext = facesContext;
        }

        @SuppressWarnings("unchecked")
        <T> T create(String componentType) {
            return (T) facesContext.getApplication().createComponent(facesContext, componentType, null);
        }
    }
}
melloware commented 4 months ago

I wonder if nobody has tried this yet and the Quarkus MyFaces extension is not scanning for @View. I will investigate.

melloware commented 4 months ago

Standalone Jetty runnner of the example above just so i could make sure it works outside of Quarkus. quarkus-444.zip

mvn clean jetty:run and navigate to http://localhost:8080/facelet.xhtml

melloware commented 4 months ago

I opened a MyFaces Quarkus Extension ticket: https://issues.apache.org/jira/browse/MYFACES-4668

melloware commented 4 months ago

Also opened this: https://issues.apache.org/jira/browse/MYFACES-4669

melloware commented 4 months ago

I have a fix for Quarkus but waiting to hear back on the escaping issue with the UIOutput

melloware commented 4 months ago

ok for MyFaces I had to update your example to be this using DocType and removing the <html> tag.

package org.primefaces.test;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.annotation.View;
import jakarta.faces.application.StateManager;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.html.HtmlBody;
import jakarta.faces.component.html.HtmlCommandButton;
import jakarta.faces.component.html.HtmlDoctype;
import jakarta.faces.component.html.HtmlForm;
import jakarta.faces.component.html.HtmlOutputText;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.facelets.Facelet;

@View("/facelet.xhtml")
@ApplicationScoped
public class FaceletView extends Facelet {

    @Override
    public void apply(FacesContext facesContext, UIComponent parent) {
        if (!facesContext.getAttributes().containsKey(StateManager.IS_BUILDING_INITIAL_STATE)) {
            return;
        }

        var components = new ComponentBuilder(facesContext);
        var rootChildren = parent.getChildren();

        var htmlDoctype = new HtmlDoctype();
        htmlDoctype.setRootElement("html");
        rootChildren.add(htmlDoctype);

        HtmlBody body = components.create(HtmlBody.COMPONENT_TYPE);
        rootChildren.add(body);

        HtmlForm form = components.create(HtmlForm.COMPONENT_TYPE);
        form.setId("form");
        body.getChildren().add(form);

        HtmlOutputText message = components.create(HtmlOutputText.COMPONENT_TYPE);
        message.setId("message");

        HtmlCommandButton actionButton = components.create(HtmlCommandButton.COMPONENT_TYPE);
        actionButton.setId("button");
        actionButton.addActionListener(
                e -> message.setValue("Hello, World! Welcome to Faces 4.0 on Jakarta EE 10"));
        actionButton.setValue("Greet");

        form.getChildren().add(actionButton);

        body.getChildren().add(message);
    }

    private static class ComponentBuilder {
        FacesContext facesContext;

        ComponentBuilder(FacesContext facesContext) {
            this.facesContext = facesContext;
        }

        @SuppressWarnings("unchecked")
        <T> T create(String componentType) {
            try {
                return (T) facesContext.getApplication().createComponent(componentType);
            } catch (ClassCastException e) {
                throw new IllegalArgumentException("Component type " + componentType + " is not valid.", e);
            }
        }
    }
}
melloware commented 4 months ago

I also asked on Mojarra which is the correct behavior https://github.com/eclipse-ee4j/mojarra/issues/5452

melloware commented 4 months ago

Got to the bottom of it. Mojarra is wrong and Myfaces is correct: https://github.com/jakartaee/faces/issues/1796

To do it correctly escape must be set on the UI output like this.

package org.primefaces.test;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.annotation.View;
import jakarta.faces.application.StateManager;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.component.html.HtmlBody;
import jakarta.faces.component.html.HtmlCommandButton;
import jakarta.faces.component.html.HtmlDoctype;
import jakarta.faces.component.html.HtmlForm;
import jakarta.faces.component.html.HtmlOutputText;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.facelets.Facelet;

@View("/facelet.xhtml")
@ApplicationScoped
public class FaceletView extends Facelet {

    @Override
    public void apply(FacesContext facesContext, UIComponent parent) {
        if (!facesContext.getAttributes().containsKey(StateManager.IS_BUILDING_INITIAL_STATE)) {
            return;
        }

        var components = new ComponentBuilder(facesContext);
        var rootChildren = parent.getChildren();

        var htmlDoctype = new HtmlDoctype();
        htmlDoctype.setRootElement("html");
        rootChildren.add(htmlDoctype);

        UIOutput output = new UIOutput();
        output.setValue("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
        output.getAttributes().put("escape", false);
        rootChildren.add(output);

        HtmlBody body = components.create(HtmlBody.COMPONENT_TYPE);
        rootChildren.add(body);

        HtmlForm form = components.create(HtmlForm.COMPONENT_TYPE);
        form.setId("form");
        body.getChildren().add(form);

        HtmlOutputText message = components.create(HtmlOutputText.COMPONENT_TYPE);
        message.setId("message");

        HtmlCommandButton actionButton = components.create(HtmlCommandButton.COMPONENT_TYPE);
        actionButton.setId("button");
        actionButton.addActionListener(
                e -> message.setValue("Hello, World! Welcome to Faces 4.0 on Jakarta EE 10"));
        actionButton.setValue("Greet");

        form.getChildren().add(actionButton);

        body.getChildren().add(message);

        output = new UIOutput();
        output.setValue("</html>");
        output.getAttributes().put("escape", false);
        rootChildren.add(output);
    }

    private static class ComponentBuilder {
        FacesContext facesContext;

        ComponentBuilder(FacesContext facesContext) {
            this.facesContext = facesContext;
        }

        @SuppressWarnings("unchecked")
        <T> T create(String componentType) {
            try {
                return (T) facesContext.getApplication().createComponent(componentType);
            } catch (ClassCastException e) {
                throw new IllegalArgumentException("Component type " + componentType + " is not valid.", e);
            }
        }
    }
}