Rewrote IndividualListController to handle error conditions from within FreeMarker. Reorganized controller control flow to account for interdependencies between body and title.

This commit is contained in:
rjy7 2010-05-25 19:20:25 +00:00
parent f40e2d1af7
commit fbdba79833
11 changed files with 307 additions and 280 deletions

View file

@ -2,7 +2,9 @@
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
@ -24,6 +26,13 @@ public class AboutController extends FreeMarkerHttpServlet {
body.put("aboutText", portal.getAboutText());
body.put("acknowledgeText", portal.getAcknowledgeText());
// Test of #list directive in template on undefined, null, or empty values
// Basic idea: empty list okay, null or undefined value not okay
// List<String> apples = new ArrayList<String>(); // no error
// List<String> apples = null; // error
// body.put("apples", apples);
// no apples in body: error
String bodyTemplate = "about.ftl";
return mergeBodyToTemplate(bodyTemplate, body);

View file

@ -18,12 +18,11 @@ import edu.cornell.mannlib.vitro.webapp.dao.filtering.filters.VitroFilterUtils;
import edu.cornell.mannlib.vitro.webapp.dao.filtering.filters.VitroFilters;
import edu.cornell.mannlib.vitro.webapp.flags.PortalFlag;
import edu.cornell.mannlib.vitro.webapp.view.VClassGroupView;
import freemarker.template.SimpleSequence;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import freemarker.template.*;
import javax.servlet.ServletContext;
import java.util.*;
@ -69,8 +68,10 @@ public class BrowseController extends FreeMarkerHttpServlet {
protected String getBody() {
Map body = new HashMap();
Map<String, Object> body = new HashMap<String, Object>();
String bodyTemplate = "classGroups.ftl";
String message = null;
// Set main page template attributes specific to this page
// But the template should control this! Try putting in a div inside the content.
//root.put("contentClass", "siteMap");
@ -80,22 +81,25 @@ public class BrowseController extends FreeMarkerHttpServlet {
//PortalFlag portalState= vreq.getPortalFlag();
String message = "";
List<VClassGroup> groups = getGroups(vreq.getWebappDaoFactory().getVClassGroupDao(), vreq.getPortal().getPortalId());
List<VClassGroup> groups = getGroups(vreq.getWebappDaoFactory().getVClassGroupDao(), portalId);
if (groups == null || groups.isEmpty()) {
message = "There are not yet any items in the system.";
body.put("message", message);
}
else {
List<VClassGroupView> vcgroups = new ArrayList<VClassGroupView>(groups.size());
Iterator<VClassGroup> i = groups.iterator();
while (i.hasNext()) {
vcgroups.add(new VClassGroupView(i.next()));
// FreeMarker will wrap vcgroups in a SimpleSequence. So do we want to create the SimpleSequence directly?
// But, makes code less portable to another system.
// SimpleSequence vcgroups = new SimpleSequence(groups.size());
List<VClassGroupView> vcgroups = new ArrayList<VClassGroupView>(groups.size());
for (VClassGroup g: groups) {
vcgroups.add(new VClassGroupView(g));
}
body.put("classGroups", vcgroups);
}
String bodyTemplate = "classGroups.ftl";
}
if (message != null) {
body.put("message", message);
}
return mergeBodyToTemplate(bodyTemplate, body);
}

View file

@ -66,17 +66,12 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
protected String appName;
protected Map<String, Object> root = new HashMap<String, Object>();
// Some servlets have their own doGet() method, in which case they need to call
// doSetup(), setTitle(), setBody(), and write() themselves. Other servlets define only
// a getBody() and getTitle() method and use the parent doGet() method.
public void doGet( HttpServletRequest request, HttpServletResponse response )
throws IOException, ServletException {
try {
callSuperGet(request, response); // ??
doSetup(request, response);
setTitle();
setBody();
setTitleAndBody();
write(response);
} catch (Throwable e) {
@ -90,89 +85,22 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
throws ServletException, IOException {
doGet(request, response);
}
protected void setBody() {
root.put("body", getBody());
}
protected void setSharedVariable(String key, Object value) {
try {
config.setSharedVariable(key, value);
} catch (TemplateModelException e) {
log.error("Can't set shared variable '" + key + "'.");
}
}
protected void setTitle() {
setSharedVariable("title", getTitle());
}
protected String getTitle() {
return null;
}
protected String getBody() {
return null;
}
protected StringWriter mergeToTemplate(String templateName, Map<String, Object> map) {
Template template = null;
try {
template = config.getTemplate(templateName);
} catch (IOException e) {
log.error("Cannot get template " + templateName);
}
StringWriter sw = new StringWriter();
if (template != null) {
try {
template.process(map, sw);
} catch (TemplateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return sw;
}
protected String mergeBodyToTemplate(String templateName, Map<String, Object> map) {
templateName = "body/" + templateName;
String body = mergeToTemplate(templateName, map).toString();
return body;
}
protected void write(HttpServletResponse response) {
String templateName = "page/default.ftl";
StringWriter sw = mergeToTemplate(templateName, root);
try {
PrintWriter out = response.getWriter();
out.print(sw);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
protected void callSuperGet(HttpServletRequest request, HttpServletResponse response) {
try {
super.doGet(request,response);
} catch (ServletException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// RY This needs to be broken out as is for FreeMarkerComponentGenerator, which should not
// include callSuperGet(). So it's only temporary.
// Basic setup needed by all controllers
protected void doSetup(HttpServletRequest request, HttpServletResponse response) {
if ( !(this instanceof FreeMarkerComponentGenerator) ) {
try {
super.doGet(request,response);
} catch (ServletException e) {
log.error("Servlet exception calling VitroHttpRequest.doGet()");
e.printStackTrace();
} catch (IOException e) {
log.error("IO exception calling VitroHttpRequest.doGet()");
e.printStackTrace();
}
}
vreq = new VitroRequest(request);
this.response = response;
portal = vreq.getPortal();
@ -210,6 +138,34 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
setSharedVariable("stylesheets", new StylesheetList(themeDir));
setSharedVariable("scripts", new ScriptList());
}
// Define template locations. Template loader will look first in the theme-specific
// location, then in the vitro location.
// RY We cannot do this in FreeMarkerSetup because (a) the theme depends on the portal,
// and we have multi-portal installations, and (b) we need to support theme-switching on the fly.
// To make more efficient, we could do this once, and then have a listener that does it again
// when theme is switched. BUT this doesn't support (a), only (b), so we have to do it on every request.
protected final void setTemplateLoader() {
String themeTemplateDir = context.getRealPath(getThemeDir()) + "/ftl";
String vitroTemplateDir = context.getRealPath("/templates/freemarker");
try {
FileTemplateLoader themeFtl = new FileTemplateLoader(new File(themeTemplateDir));
FileTemplateLoader vitroFtl = new FileTemplateLoader(new File(vitroTemplateDir));
ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");
TemplateLoader[] loaders = new TemplateLoader[] { themeFtl, vitroFtl, ctl };
MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);
config.setTemplateLoader(mtl);
} catch (IOException e) {
log.error("Error loading templates");
}
}
private TabMenu getTabMenu() {
return new TabMenu(vreq, portalId);
}
public String getThemeDir() {
return portal.getThemeDir().replaceAll("/$", "");
@ -252,31 +208,31 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
setSharedVariable("urls", urls);
}
private final void setLoginInfo() {
String loginName = null;
int securityLevel;
HttpSession session = vreq.getSession();
LoginFormBean loginBean = (LoginFormBean) session.getAttribute("loginHandler");
if (loginBean != null && loginBean.testSessionLevel(vreq) > -1) {
loginName = loginBean.getLoginName();
securityLevel = Integer.parseInt(loginBean.getLoginRole());
}
if (loginName != null) {
root.put("loginName", loginName);
private final void setLoginInfo() {
String loginName = null;
int securityLevel;
HttpSession session = vreq.getSession();
LoginFormBean loginBean = (LoginFormBean) session.getAttribute("loginHandler");
if (loginBean != null && loginBean.testSessionLevel(vreq) > -1) {
loginName = loginBean.getLoginName();
securityLevel = Integer.parseInt(loginBean.getLoginRole());
}
if (loginName != null) {
root.put("loginName", loginName);
securityLevel = Integer.parseInt(loginBean.getLoginRole());
if (securityLevel >= FILTER_SECURITY_LEVEL) {
ApplicationBean appBean = vreq.getAppBean();
if (appBean.isFlag1Active()) {
root.put("showFlag1SearchField", true);
}
}
}
}
private final void setCopyrightInfo() {
securityLevel = Integer.parseInt(loginBean.getLoginRole());
if (securityLevel >= FILTER_SECURITY_LEVEL) {
ApplicationBean appBean = vreq.getAppBean();
if (appBean.isFlag1Active()) {
root.put("showFlag1SearchField", true);
}
}
}
}
private final void setCopyrightInfo() {
String copyrightText = portal.getCopyrightAnchor();
if ( ! StringUtils.isEmpty(copyrightText) ) {
@ -289,13 +245,13 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
copyright.put("url", portal.getCopyrightURL());
root.put("copyright", copyright);
}
}
private final void setThemeInfo(String themeDir) {
}
private final void setThemeInfo(String themeDir) {
// This value will be available to any template as a path for adding a new stylesheet.
// It does not contain the context path, because the methods to generate the href
// attribute from the string passed in by the template automatically add the context path.
// This value will be available to any template as a path for adding a new stylesheet.
// It does not contain the context path, because the methods to generate the href
// attribute from the string passed in by the template automatically add the context path.
setSharedVariable("stylesheetDir", themeDir + "/css");
String themeDirWithContext = getUrl(themeDir);
@ -305,41 +261,107 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
setSharedVariable("siteIconPath", themeDirWithContext + "/site_icons");
}
// Define template locations. Template loader will look first in the theme-specific
// location, then in the vitro location.
// RY We cannot do this in FreeMarkerSetup because (a) the theme depends on the portal,
// and we have multi-portal installations, and (b) we need to support theme-switching on the fly.
// To make more efficient, we could do this once, and then have a listener that does it again
// when theme is switched. BUT this doesn't support (a), only (b), so we have to do it on every request.
protected final void setTemplateLoader() {
String themeTemplateDir = context.getRealPath(getThemeDir()) + "/ftl";
String vitroTemplateDir = context.getRealPath("/templates/freemarker");
try {
FileTemplateLoader themeFtl = new FileTemplateLoader(new File(themeTemplateDir));
FileTemplateLoader vitroFtl = new FileTemplateLoader(new File(vitroTemplateDir));
ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");
TemplateLoader[] loaders = new TemplateLoader[] { themeFtl, vitroFtl, ctl };
MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);
config.setTemplateLoader(mtl);
} catch (IOException e) {
log.error("Error loading templates");
}
}
}
private TabMenu getTabMenu() {
return new TabMenu(vreq, portalId);
// Default case is to set title first, because it's used in the body. However, in some cases
// the title is based on values computed during compilation of the body (e.g., IndividualListController).
// Individual controllers can override this method to set title and body together. End result must be:
// body is added to root with key "body"
// title is set as a shared variable with key "title"
// This can be achieved by making sure setBody() and setTitle() are called.
protected void setTitleAndBody() {
setTitle();
setBody();
}
protected void setBody() {
root.put("body", getBody());
}
protected String getBody() {
return ""; // body should never be null
}
protected void setTitle() {
String title = getTitle();
// If the individual controller fails to assign a non-null, non-empty title
if (StringUtils.isEmpty(title)) {
title = appName;
}
// Title is a shared variable because it's used in both body and head elements.
setSharedVariable("title", title);
}
protected String getTitle() {
return "";
}
protected void setSharedVariable(String key, Object value) {
try {
config.setSharedVariable(key, value);
} catch (TemplateModelException e) {
log.error("Can't set shared variable '" + key + "'.");
}
}
protected StringWriter mergeToTemplate(String templateName, Map<String, Object> map) {
Template template = null;
try {
template = config.getTemplate(templateName);
} catch (IOException e) {
log.error("Cannot get template " + templateName);
}
StringWriter sw = new StringWriter();
if (template != null) {
try {
template.process(map, sw);
} catch (TemplateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return sw;
}
protected String mergeBodyToTemplate(String templateName, Map<String, Object> map) {
templateName = "body/" + templateName;
String body = mergeToTemplate(templateName, map).toString();
return body;
}
protected void write(HttpServletResponse response) {
String templateName = "page/" + getPageTemplateName();
StringWriter sw = mergeToTemplate(templateName, root);
try {
PrintWriter out = response.getWriter();
out.print(sw);
} catch (IOException e) {
log.error("FreeMarkerHttpServlet cannot write output");
e.printStackTrace();
}
}
// Can be overridden by individual controllers
protected String getPageTemplateName() {
return "default.ftl";
}
public static boolean isConfigured() {
return config != null;
}
// TEMPORARY for transition from JSP to FreeMarker. Once transition
// is complete and no more pages are generated in JSP, this can be removed.
// Do this if FreeMarker is configured (i.e., not Datastar) and if we are not in
// a FreeMarkerHttpServlet, which will generate identity, menu, and footer from the page template.
// It's a static method because it needs to be called from JSPs that don't go through a servlet.
// It's a static method because it needs to be called from JSPs that don't go through a servlet.
public static void getFreeMarkerComponentsForJsp(HttpServletRequest request, HttpServletResponse response) {
FreeMarkerComponentGenerator fcg = new FreeMarkerComponentGenerator(request, response);
request.setAttribute("ftl_identity", fcg.getIdentity());
@ -347,12 +369,8 @@ public class FreeMarkerHttpServlet extends VitroHttpServlet {
request.setAttribute("ftl_search", fcg.getSearch());
request.setAttribute("ftl_footer", fcg.getFooter());
}
public static boolean isConfigured() {
return config != null;
}
/* ******************** Utilities ******************* */
/* ******************** Static utilities ******************* */
public static String getUrl(String path) {
if ( ! path.startsWith("/") ) {

View file

@ -2,144 +2,119 @@
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.beans.VClassGroup;
import edu.cornell.mannlib.vitro.webapp.utils.StringUtils;
import edu.cornell.mannlib.vitro.webapp.view.IndividualView;
/**
* Generates a list of individuals for display in a template
*/
public class IndividualListController extends FreeMarkerHttpServlet {
long startTime = -1;
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(IndividualListController.class.getName());
private VClass vclass = null;
/**
* This generates a list of entities and then sends that
* off to a jsp to be displayed.
*
* Expected parameters:
*
* Expected Attributes:
* entity - set to entity to display properties for.
*
* @author bdc34
*/
private String title = null;
// TODO Rewrite error cases to use FreeMarker templates. Restructure so we're always doing the body
// and then calling writeOutput().
public void doGet( HttpServletRequest req, HttpServletResponse res )
throws IOException, ServletException {
startTime = System.currentTimeMillis(); // TODO: remove
protected void setTitleAndBody() {
setBody();
}
protected String getBody() {
Map<String, Object> body = new HashMap<String, Object>();
String bodyTemplate = "individualList.ftl";
String errorMessage = null;
String message = null;
try {
super.doSetup(req, res);
Object obj = vreq.getAttribute("vclass");
vclass=null;
if( obj == null ) { // look for vitroclass id parameter
String vitroClassIdStr=req.getParameter("vclassId");
if (vitroClassIdStr!=null && !vitroClassIdStr.equals("")) {
vclass = null;
if ( obj == null ) { // look for vitroclass id parameter
String vitroClassIdStr = vreq.getParameter("vclassId");
if ( !StringUtils.isEmpty(vitroClassIdStr)) {
try {
//TODO have to change this so vclass's group and entity count are populated
vclass = vreq.getWebappDaoFactory().getVClassDao().getVClassByURI(vitroClassIdStr);
if (vclass == null) {
log.error("Couldn't retrieve vclass "+vitroClassIdStr);
response.sendRedirect(Routes.BROWSE + "?"+vreq.getQueryString());
}
} catch (Exception ex) {
throw new HelpException("IndividualListController: request parameter 'vclassId' must be a URI string");
//TODO have to change this so vclass's group and entity count are populated
vclass = vreq.getWebappDaoFactory().getVClassDao().getVClassByURI(vitroClassIdStr);
if (vclass == null) {
log.error("Couldn't retrieve vclass " + vitroClassIdStr);
errorMessage = "Class " + vitroClassIdStr + " not found";
}
} catch (Exception ex) {
throw new HelpException("IndividualListController: request parameter 'vclassId' must be a URI string.");
}
}
} else if (obj instanceof VClass) {
vclass = (VClass)obj;
} else {
throw new HelpException("IndividualListController: attribute 'vclass' must be of type "
+ VClass.class.getName() );
+ VClass.class.getName() + ".");
}
if (vclass!=null){
setBody();
write(response);
}
// RY Rewrite error cases for FreeMarker, not JSP
if (vclass != null) {
// Create list of individual view objects
List<Individual> individualList = vreq.getWebappDaoFactory().getIndividualDao().getIndividualsByVClass(vclass);
List<IndividualView> individuals = new ArrayList<IndividualView>(individualList.size());
if (individualList == null) {
// RY Is this really an error?
log.error("individuals list is null");
message = "No individuals to display.";
} else {
for (Individual i: individualList) {
individuals.add(new IndividualView(i));
}
}
// Set title and subtitle. Title will be retrieved later in getTitle().
VClassGroup classGroup = vclass.getGroup();
if (classGroup == null) {
title = vclass.getName();
} else {
title = classGroup.getPublicName();
body.put("subtitle", vclass.getName());
}
body.put("individuals", individuals);
}
} catch (HelpException help){
doHelp(response);
errorMessage = "Request attribute 'vclass' or request parameter 'vclassId' must be set before calling. Its value must be a class uri.";
} catch (Throwable e) {
vreq.setAttribute("javax.servlet.jsp.jspException",e);
RequestDispatcher rd = req.getRequestDispatcher("/error.jsp");
rd.forward(vreq, response);
bodyTemplate = "error.ftl";
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,IOException {
doGet(request, response);
}
protected String getBody() {
Map<String, Object> body = new HashMap<String, Object>();
if (errorMessage != null) {
bodyTemplate = "errorMessage.ftl";
body.put("errorMessage", errorMessage);
} else if (message != null) {
body.put("message", message);
}
// Create list of individuals
List<Individual> individualList = vreq.getWebappDaoFactory().getIndividualDao().getIndividualsByVClass(vclass);
List<IndividualView> individuals = new ArrayList<IndividualView>(individualList.size());
Iterator<Individual> i = individualList.iterator();
while (i.hasNext()) {
individuals.add(new IndividualView(i.next()));
}
body.put("individuals", individuals);
// But the JSP version includes url rewriting via URLRewritingHttpServletResponse
// RY *** FIX - define getUrl method of IndividualView
body.put("individualUrl", getUrl("/entity?home=" + portalId + "&uri="));
if (individuals == null) {
log.error("individuals list is null");
}
// Use instead of getTitle(), because we have a subtitle too
String title = "";
VClassGroup classGroup=vclass.getGroup();
if (classGroup==null) {
title = vclass.getName();
} else {
title = classGroup.getPublicName();
setSharedVariable("subTitle", vclass.getName());
}
setSharedVariable("title", title);
String templateName = "individualList.ftl";
return mergeBodyToTemplate(templateName, body);
setTitle();
return mergeBodyToTemplate(bodyTemplate, body);
}
// RY Rewrite as a template
private void doHelp(HttpServletResponse res)
throws IOException, ServletException {
ServletOutputStream out = res.getOutputStream();
res.setContentType("text/html; charset=UTF-8");
out.println("<html><body><h2>Quick Notes on using EntityList:</h2>");
out.println("<p>request.attributes 'entities' must be set by servlet before calling."
+" It must be a List of Entity objects </p>");
out.println("</body></html>");
protected String getTitle() {
// The title is determined during compilation of the body, so we put it in an instance variable
// to be retrieved later.
return title;
}
private class HelpException extends Throwable {
private static final long serialVersionUID = 1L;
private class HelpException extends Throwable{
public HelpException(String string) {
super(string);
}

View file

@ -11,7 +11,7 @@ public class Routes {
public static final String BROWSE = "/browse";
public static final String COMMENT_FORM = "/comments";
public static final String INDIVIDUAL = "/individual";
public static final String INDIVIDUAL_LIST = "/entitylist"; // "/individuallist";
public static final String INDIVIDUAL_LIST = "/individuallist"; // "/entitylist"; "/individuallist";
public static final String SEARCH = "/search";
public static final String TERMS_OF_USE = "/termsOfUse";

View file

@ -25,6 +25,8 @@ public class IndividualView extends ViewObject {
return individual.getName();
}
// RY However, the moniker should undergo p:process but the class name shouldn't!
// So, it needs to be callable from Java.
public String getTagline() {
String tagline = individual.getMoniker();
return StringUtils.isEmpty(tagline) ? individual.getVClass().getName() : tagline;

View file

@ -11,9 +11,9 @@
<ul>
<#list classGroup.classes as class>
<li><a href="${class.url}">${class.name}</a> (${class.individualCount})</li>
</#list>
</ul>
</#list>
</div>
</#if>
</#if>

View file

@ -1,6 +1,6 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#-- Comment form -->
<#-- Contact form -->
<div class="staticPageBackground feedbackForm">

View file

@ -0,0 +1,7 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#-- Template for general system error. -->
<p>There was an error in the system.</p>
<p>Return to the <a href="${urls.home}">home page</a>.</p>

View file

@ -0,0 +1,7 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#-- Standard template to display an error message generated from any controller. Keeps this out of individual templates. -->
<#if errorMessage??>
<p>${errorMessage}</p>
</#if>

View file

@ -7,16 +7,21 @@
<div class="individualList">
<h2>${title}</h2>
<#if subtitle??>
<h4>${subTitle}"</h4>
<h4>${subtitle}</h4>
</#if>
<#-- RY NEED TO ACCOUNT FOR p:process stuff -->
<ul>
<#list individuals as individual>
<li>
<a href="${individual.profileUrl}">${individual.name}</a> ${individual.tagline}
</li>
</#list>
</ul>
</div>
<#if message??>
<p>${message}</p>
<#else>
<#-- RY NEED TO ACCOUNT FOR p:process stuff -->
<ul>
<#list individuals as individual>
<li>
<a href="${individual.profileUrl}">${individual.name}</a> ${individual.tagline}
</li>
</#list>
</ul>
</#if>
</div>
</div>