Concatenate strings in JSP EL?
Asked Answered
S

8

15

I have a List of beans, each of which has a property which itself is a List of email addresses.

<c:forEach items="${upcomingSchedule}" var="conf">
    <div class='scheduled' title="${conf.subject}" id="scheduled<c:out value="${conf.id}"/>">
    ...
    </div>
</c:forEach>

This renders one <div> per bean in the List.

For the sublist, what I'd like to be able to do is to concatenate each of the entries in the list to form a single String, to be displayed as a part of the <div>'s title attribute. Why? Because we are using a javascript library (mootools) to turn this <div> into a floating tool tip, and the library turns the title into the text of the tooltip.

So, if ${conf.subject} was "Subject", ultimately I'd like the title of the <div> to be "Subject: [email protected], [email protected], etc.", containing all of the email addresses of the sub-list.

How can I do this using JSP EL? I'm trying to stay away from putting scriptlet blocks in the jsp file.

Sangfroid answered 17/11, 2008 at 18:22 Comment(1)
Possible duplicate of JSP EL String concatenationDecasyllable
S
9

Figured out a somewhat dirty way to do this:

<c:forEach items="${upcomingSchedule}" var="conf">
    <c:set var="title" value="${conf.subject}: "/>
    <c:forEach items="${conf.invitees}" var="invitee">
        <c:set var="title" value="${title} ${invitee}, "/>
    </c:forEach>
    <div class='scheduled' title="${title}" id="scheduled<c:out value="${conf.id}"/>">
    ...
    </div>
</c:forEach>

I just use <c:set> repeatedly, referencing it's own value, to append/concatenate the strings.

Sangfroid answered 17/11, 2008 at 18:34 Comment(1)
I think this is the best you can do without writing your own function (which isn't too hard) along the lines of fn:joinLadonna
L
16

The "clean" way to do this would be to use a function. As the JSTL join function won't work on a Collection, you can write your own without too much trouble, and reuse it all over the place instead of cut-and-pasting a large chunk of loop code.

You need the function implementation, and a TLD to let your web application know where to find it. Put these together in a JAR and drop it into your WEB-INF/lib directory.

Here's an outline:

com/x/taglib/core/StringUtil.java

package com.x.taglib.core;

public class StringUtil {

  public static String join(Iterable<?> elements, CharSequence separator) {
    StringBuilder buf = new StringBuilder();
    if (elements != null) {
      if (separator == null)
        separator = " ";
      for (Object o : elements) {
        if (buf.length() > 0)
          buf.append(separator);
        buf.append(o);
      }
    }
    return buf.toString();
  }

}

META-INF/x-c.tld:

<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
  <tlib-version>1.0</tlib-version>
  <short-name>x-c</short-name>
  <uri>http://dev.x.com/taglib/core/1.0</uri>
  <function>
    <description>Join elements of an Iterable into a string.</description>
    <display-name>Join</display-name>
    <name>join</name>
    <function-class>com.x.taglib.core.StringUtil</function-class>
    <function-signature>java.lang.String join(java.lang.Iterable, java.lang.CharSequence)</function-signature>
  </function>
</taglib>

While the TLD is a little verbose, knowing your way around one is a good skill for any developer working with JSP. And, since you've chosen a standard like JSP for presentation, there's a good chance you have tools that will help you out.

This approach has many advantages over the alternative of adding more methods to the underlying model. This function can be written once, and reused in any project. It works with a closed-source, third-party library. Different delimiters can be supported in different contexts, without polluting a model API with a new method for each.

Most importantly, it supports a separation of view and model-controller development roles. Tasks in these two areas are often performed by different people at different times. Maintaining a loose coupling between these layers minimizes complexity and maintenance costs. When even a trivial change like using a different delimiter in the presentation requires a programmer to modify a library, you have a very expensive and cumbersome system.

The StringUtil class is the same whether its exposed as a EL function or not. The only "extra" necessary is the TLD, which is trivial; a tool could easily generate it.

Ladonna answered 17/11, 2008 at 20:17 Comment(3)
Why do you think is better to write a custom jstl funtion than one more method in the backing bean? It's not a provocative question. In a case like this I would write one method do to this in the bean, something like getInviteesAsString(). What's wrong with this?Photocell
I added a response to my answer. I'd pose the same question to you: why do you think it is better to change the backing bean?Ladonna
I thought it was better because it supports a separation of view and model-controller development roles, and leave as less logic as possible in the view. But, with the add to your answer, I understand that also your method supports this separation, and has also other benefits. Thanks for the add.Photocell
S
9

Figured out a somewhat dirty way to do this:

<c:forEach items="${upcomingSchedule}" var="conf">
    <c:set var="title" value="${conf.subject}: "/>
    <c:forEach items="${conf.invitees}" var="invitee">
        <c:set var="title" value="${title} ${invitee}, "/>
    </c:forEach>
    <div class='scheduled' title="${title}" id="scheduled<c:out value="${conf.id}"/>">
    ...
    </div>
</c:forEach>

I just use <c:set> repeatedly, referencing it's own value, to append/concatenate the strings.

Sangfroid answered 17/11, 2008 at 18:34 Comment(1)
I think this is the best you can do without writing your own function (which isn't too hard) along the lines of fn:joinLadonna
L
4

Could you use this? Seems like it wants an array instead of a list..

${fn:join(array, ";")}

http://java.sun.com/products/jsp/jstl/1.1/docs/tlddocs/fn/join.fn.html

Law answered 17/11, 2008 at 18:35 Comment(0)
P
1

If your sublist is an ArrayList and you do this:

<div class='scheduled' title="${conf.subject}: ${conf.invitees}" id="scheduled${conf.id}">

you obtain almost what you need.

The only difference is that the title will be: "Subject: [[email protected], [email protected], etc.]".

Maybe can be good enough for you.

Photocell answered 17/11, 2008 at 18:50 Comment(1)
That does sort of work if the underlying list is an ArrayList, but I wouldn't want to take a chance that it was some other List implementation without the same toString() implemenation.Sangfroid
S
0

I think this is what you want:

<c:forEach var="tab" items="${tabs}">
 <c:set var="tabAttrs" value='${tabAttrs} ${tab.key}="${tab.value}"'/>
</c:forEach>

In this case, I had a hashmap with tab ID (key) and URL (value). The tabAttrs variable isn't set prior to this. So it just sets the value to the current value of tabAttrs ('' to start) plus the key/value expression.

Supervision answered 23/1, 2010 at 0:20 Comment(1)
The OP already answered his own question :) Check the message with the big green checkmark.Polyhymnia
I
0

Just put the string byside the var from server, like this:

<c:forEach items="${upcomingSchedule}" var="conf">
    <div class='scheduled' title="${conf.subject}" 

         id="scheduled${conf.id}">

    ...
    </div>
</c:forEach>

Too late!!!

Iyre answered 27/1, 2013 at 18:1 Comment(0)
C
0

The way tag libraries are implemented seems to have moved on considerably since this answer was originally posted so I ended up making some drastic changes to get things working. My final result was:

Tag Library File:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.1" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd">
  <tlib-version>1.0</tlib-version>
  <short-name>string_util</short-name>
  <uri>/WEB-INF/tlds/string_util</uri>
  <info>String Utilities</info>
  <tag>
    <name>join</name>
    <info>Join the contents of any iterable using a separator</info>
    <tag-class>XXX.taglib.JoinObjects</tag-class>
    <body-content>tagdependent</body-content>
    <attribute>
      <name>iterable</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
      <type>java.lang.Iterable</type>
    </attribute>
    <attribute>
      <name>separator</name>
      <required>false</required>
      <rtexprvalue>false</rtexprvalue>
      <type>java.lang.String</type>
    </attribute>
  </tag>

  <tag>
    <name>joinints</name>
    <info>Join the contents of an integer array using a separator</info>
    <tag-class>XXX.taglib.JoinInts</tag-class>
    <body-content>tagdependent</body-content>
    <attribute>
      <name>integers</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
      <type>java.lang.Integer[]</type>
    </attribute>
    <attribute>
      <name>separator</name>
      <required>false</required>
      <rtexprvalue>false</rtexprvalue>
      <type>java.lang.String</type>
    </attribute>
  </tag>
</taglib>

JoinInts.java

public class JoinInts extends TagSupport {

    int[] integers;
    String separator = ",";

    @Override
    public int doStartTag() throws JspException {
        if (integers != null) {
            StringBuilder buf = new StringBuilder();
            if (separator == null) {
                separator = " ";
            }
            for (int i: integers) {
                if (buf.length() > 0) {
                    buf.append(separator);
                }
                buf.append(i);
            }
            try {
                pageContext.getOut().print(buf);
            } catch (IOException ex) {
                Logger.getLogger(JoinInts.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return SKIP_BODY;
    }

    @Override
    public int doEndTag() throws JspException {
        return EVAL_PAGE;
    }

    public int[] getIntegers() {
        return integers;
    }

    public void setIntegers(int[] integers) {
        this.integers = integers;
    }

    public String getSeparator() {
        return separator;
    }

    public void setSeparator(String separator) {
        this.separator = separator;
    }
}

To use it:

<%@ taglib prefix="su" uri="/WEB-INF/tlds/string_util.tld" %>

[new Date(${row.key}), <su:joinints integers="${row.value}" separator="," />],
Curlpaper answered 17/1, 2014 at 11:12 Comment(0)
A
0

You can use the EL 3.0 Stream API. For example if you have a list of strings,

<div>${stringList.stream().reduce(",", (n,p)->p.concat(n))}</div>

In case you have a list of objects for ex. Person(firstName, lastName) and you wish to concat only one property of them (ex firstName) you can use map,

<div>${personList.stream().map(p->p.getFirstName()).reduce(",", (n,p)->p.concat(n))}</div>

In your case you could use something like that (remove the last ',' also),

<c:forEach items="${upcomingSchedule}" var="conf">
    <c:set var="separator" value=","/>
    <c:set var="titleFront" value="${conf.subject}: "/>
    <c:set var="titleEnd" value="${conf.invitees.stream().reduce(separator, (n,p)->p.concat(n))}"/>
    <div class='scheduled' title="${titleFront} ${titleEnd.isEmpty() ? "" : titleEnd.substring(0, titleEnd.length()-1)}" id="scheduled<c:out value="${conf.id}"/>">
    ...
    </div>
</c:forEach>

Be careful! The EL 3.0 Stream API was finalized before the Java 8 Stream API and it is different than that. They can't sunc both apis because it will break the backward compatibility.

Ampulla answered 20/9, 2017 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.