0

I've searched for a while and didn't find a solution, so if it already exists somewhere I apologize.

I currently have a menu being dynamically generated from a database. After the menu is built I want to add a sub menu at the bottom with two reports. One of the reports generates a PDF document to download. I've been able to use the <p:fileDownload/> tag with a <p:menuitem/> tag and have been successful.

The problem I'm running into with my dynamic menu is that I'm using a model in the bean to generate it. And I haven't been able to find a way to add the download functionality to a DefaultMenuItem object that is being added to the model. Below is a sample of my code so far (I'm only going to show one report being added to simplify my code. In my example I'll only add one menu item to download a file):

My xhtml:

<h:form id="menu">
  <p:menu toggleable="true" tabindex="0" id="screenMenu" model="#{myBean.model}"/>
</h:form>

My Bean:

@ManagedBean(name = "myBean")
@ViewScoped
public class MyBean implements Serializable {

  private ArrayList<Menu> menuItems;
  private ByteArrayOutputStream output;
  private StreamedContent file;

  public ArrayList<Menu> getMenuItems(){
    return menuItems;
  }

  public void setMenuItems(ArrayList<Menu> menuItems){
    this.menuItems = menuItems;
  }

  public StreamedContent getFile() {
    return file;
  }

  public void setFile(StreamedContent file) {
    this.file = file;
  }

  public ByteArrayOutputStream getOutput() {
    return output;
  }

  public void setOutput(ByteArrayOutputStream output) {
    this.output = output;
  }

  @PostConstruct
  public void init(){
    //Code runs here that loads up the list of menu items from database

    //Load the model with the returned menu items
    DynamicMenuModel model = new DynamicMenuModel();
    for(Menu menuItem : menuItems){
      DefaultMenuItem item = new DefaultMenuItem(menuItem.getTitle());
      item.setParam("screen", menuItem.getScreen());
      item.setCommand("#{myBean.selectMenuItem}");
      model.addElement(item);
    }

    //Extra item added to model in order to download file
    DefaultMenuItem item = new DefaultMenuItem("Download Report");
    item.addCommand("#{myBean.downloadReport}");

    /*
    *******************************************************************************
    ***How would I add code to include the <p:fileDownload/> tag functionality??***
    *******************************************************************************
    */

    model.addElement(item);
  }

  public void downloadReport(ActionEvent event){
    output = new ByteArrayOutputStream();
    /*
    Code goes here that grabs information and creates a pdf document and 
    saves it to the output object
    */

    ByteArrayInputStream stream = new ByteArrayInputStream(output.toByteArray());
    file = new DefaultStreamedContent(stream, "pdf", "report.pdf");

    FileDownloadActionListener download = new FileDownloadActionListener();
    download.processAction(event); //<--It's not liking this
  }

  public String selectMenuItem(ActionEvent event){
    MenuItem menuItem = ((MenuActionEvent) event).getMenuItem();
    String screen = menuItem.getParams().get("screen").get(0);
    return "/" + screen + "?faces-redirect=true";
  }
}

I've tried making the "downloadReport" method have an ActionEvent parameter and pass that into the processAction() method of a newly created FileDownloadActionListener object, but how that would even work doesn't make sense to me. Thought I'd try it anyway.

Let me know if you need any other information from me. Any syntax errors or anything that would make this code not work is purely from me condensing a bunch of code in the little bit I've shown. Right now it runs fine if I use the <p:fileDownload/> tag with a separate menu. I just can't do that with my dynamic menu because the <ui:repeat> tag doesn't work inside the <p:menu> tag.

Any advice would be helpful.

Thanks!

Mehdi Bouzidi
  • 1,937
  • 3
  • 15
  • 31
P-ratt
  • 249
  • 4
  • 13
  • Tried `c:foreach` instead of the `ui:repeat`? – Kukeltje Sep 02 '17 at 22:58
  • Currently I'm using Primefaces. c:forEach is part of the JSP Standard Tag Library and isn't compatible with the p:menu tag I'm using. – P-ratt Sep 05 '17 at 15:58
  • Why is it not compatible? – Kukeltje Sep 05 '17 at 16:45
  • Not sure why it's not compatible. I just know it doesn't work. Neither c:forEach nor ui:repeat work inside the p:menu tag. I think the p:menu tag will only parse children if the children are of the UIComponent type instead of the instruction type. – P-ratt Sep 06 '17 at 15:32
  • Your last sentence is true when render phase, but the c:foreach is processed view build time https://stackoverflow.com/questions/3342984/jstl-in-jsf2-facelets-makes-sense so it should at least do something – Kukeltje Sep 06 '17 at 19:07

1 Answers1

1

You almost got it! Some hints:

  1. As commented by Çağatay Çivici for this PrimeFaces issue, FileDownloadActionListener can not be created dynamically when using the MenuModel.
  2. By looking at the source code of the FileDownloadActionListener you can see that the StreamedContent to download is created by executing the ValueExpression which has to be specified as constructor parameter.
  3. Set ajax=false for the DefaultMenuItem.

So the working version of the bean looks like this:

@ManagedBean(name = "myBean")
@ViewScoped
public class MyBean implements Serializable {

  private ArrayList<Menu> menuItems;
  private ByteArrayOutputStream output;
  private StreamedContent file;

  public ArrayList<Menu> getMenuItems(){
    return menuItems;
  }

  public void setMenuItems(ArrayList<Menu> menuItems){
    this.menuItems = menuItems;
  }

  public StreamedContent getFile() {
    return file;
  }

  public void setFile(StreamedContent file) {
    this.file = file;
  }

  public ByteArrayOutputStream getOutput() {
    return output;
  }

  public void setOutput(ByteArrayOutputStream output) {
    this.output = output;
  }

  @PostConstruct
  public void init(){
    //Code runs here that loads up the list of menu items from database

    //Load the model with the returned menu items
    DynamicMenuModel model = new DynamicMenuModel();
    for(Menu menuItem : menuItems){
      DefaultMenuItem item = new DefaultMenuItem(menuItem.getTitle());
      item.setParam("screen", menuItem.getScreen());
      item.setCommand("#{myBean.selectMenuItem}");
      model.addElement(item);
    }

    //Extra item added to model in order to download file
    DefaultMenuItem item = new DefaultMenuItem("Download Report");

    //Add a call to the method that initializes the file download
    item.addCommand("#{myBean.callDownloadReport}");
    item.setAjax(false);

    model.addElement(item);
  }

  public void callDownloadReport(MenuActionEvent menuActionEvent){
    //Create new action event
    final ActionEvent actionEvent = new ActionEvent(menuActionEvent.getComponent());

    //Create the value expression for the download listener
    //-> is executed when calling "processAction"!
    final FacesContext context = FacesContext.getCurrentInstance();
    final String exprStr = "#{myBean.downloadReport}";
    final ValueExpression valueExpr = context.getApplication()
      .getExpressionFactory()
      .createValueExpression(context.getELContext(), exprStr, StreamedContent.class);

    //Instantiate the download listener and indirectly call "downloadReport()"
    new FileDownloadActionListener(valueExpr, null, null)
            .processAction(actionEvent);
  }

  /**
   * Indirectly called by {@link FileDownloadActionListener#processAction(ActionEvent)}.
   */
  public StreamedContent downloadReport(){
    output = new ByteArrayOutputStream();
    /*
    Code goes here that grabs information and creates a pdf document and 
    saves it to the output object
    */

    final ByteArrayInputStream stream = new ByteArrayInputStream(output.toByteArray());

    //Return the streamed content to the download listener
    //->returns the stream to the client
    return new DefaultStreamedContent(stream, "pdf", "report.pdf");
  }

  public String selectMenuItem(ActionEvent event){
    MenuItem menuItem = ((MenuActionEvent) event).getMenuItem();
    String screen = menuItem.getParams().get("screen").get(0);
    return "/" + screen + "?faces-redirect=true";
  }

}

Additional hint:

An optional parameter for selecting the report can be added as parameter to the ValueExpression and is handed over to the method that initializes the StreamedContent:

@ManagedBean(name = "myBean")
@ViewScoped
public class MyBean implements Serializable {

  private static final String REPORT_IDENTIFIER = "report_id";

  @PostConstruct
  public void init(){
    //Initialize menu model
    //...

    //Add two menu items for downloading different reports
    final DefaultMenuItem downloadItem1 = new DefaultMenuItem("Download Report 1");
    downloadItem1.addCommand("#{myBean.callDownloadReport}");
    downloadItem1.setAjax(false);
    downloadItem1.setParam(REPORT_IDENTIFIER, "1");
    model.addElement(downloadItem1);

    final DefaultMenuItem downloadItem2 = new DefaultMenuItem("Download Report 2");
    downloadItem2.addCommand("#{myBean.callDownloadReport}");
    downloadItem2.setAjax(false);
    downloadItem2.setParam(REPORT_IDENTIFIER, "2");
    model.addElement(downloadItem2);
  }

  public void callDownloadReport(MenuActionEvent menuActionEvent){
    //Get the report identifier
    final String reportId = menuActionEvent.getMenuItem()
      .getParams()
      .get(REPORT_IDENTIFIER)
      .get(0);

    //Create new action event
    final ActionEvent actionEvent = new ActionEvent(menuActionEvent.getComponent());

    //Create the value expression with the report ID for the download listener
    //-> is executed when calling "processAction"!
    final FacesContext context = FacesContext.getCurrentInstance();
    final String exprStr = "#{myBean.downloadReport('" + reportId + "')}";
    final ValueExpression valueExpr = context.getApplication()
      .getExpressionFactory()
      .createValueExpression(context.getELContext(), exprStr, StreamedContent.class);

    //Instantiate the download listener and indirectly call "downloadReport(String)"
    new FileDownloadActionListener(valueExpr, null, null)
            .processAction(actionEvent);
  }

  /**
   * Indirectly called by {@link FileDownloadActionListener#processAction(ActionEvent)}.
   *
   * @param reportId the identifier of the report
   */
  public StreamedContent downloadReport(final String reportId){
    //Create report with specified ID
    //...
  }
}
ltlBeBoy
  • 1,242
  • 16
  • 23
  • I know it's late, but this is exactly what I was looking for. While waiting for this answer I moved on to other things and figured out a work-around. I'm glad you took the time to answer this. – P-ratt Apr 03 '20 at 03:07