001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> 016 * 017 * $Id: LayoutTagHandler.java 30553 2008-02-24 15:51:31Z atchertchian $ 018 */ 019 020package org.nuxeo.ecm.platform.forms.layout.facelets; 021 022import java.io.IOException; 023import java.io.Serializable; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029 030import javax.el.ELException; 031import javax.el.ExpressionFactory; 032import javax.el.ValueExpression; 033import javax.el.VariableMapper; 034import javax.faces.FacesException; 035import javax.faces.component.UIComponent; 036import javax.faces.view.facelets.ComponentHandler; 037import javax.faces.view.facelets.FaceletContext; 038import javax.faces.view.facelets.FaceletHandler; 039import javax.faces.view.facelets.TagAttribute; 040import javax.faces.view.facelets.TagAttributes; 041import javax.faces.view.facelets.TagConfig; 042import javax.faces.view.facelets.TagException; 043import javax.faces.view.facelets.TagHandler; 044 045import org.apache.commons.lang.StringUtils; 046import org.apache.commons.logging.Log; 047import org.apache.commons.logging.LogFactory; 048import org.nuxeo.ecm.platform.forms.layout.api.Layout; 049import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition; 050import org.nuxeo.ecm.platform.forms.layout.facelets.dev.DevTagHandler; 051import org.nuxeo.ecm.platform.forms.layout.facelets.dev.LayoutDevTagHandler; 052import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager; 053import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory; 054import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils; 055import org.nuxeo.runtime.api.Framework; 056 057import com.sun.faces.facelets.el.VariableMapperWrapper; 058import com.sun.faces.facelets.tag.ui.DecorateHandler; 059 060/** 061 * Layout tag handler. 062 * <p> 063 * Computes a layout in given facelet context, for given mode and value attributes. The layout can either be computed 064 * from a layout definition, or by a layout name, where the layout service will lookup the corresponding definition. 065 * <p> 066 * If a template is found for this layout, include the corresponding facelet and use facelet template features to 067 * iterate over rows and widgets. 068 * <p> 069 * Since 5.6, the layout name attribute also accepts a comma separated list of layout names. 070 * 071 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> 072 */ 073public class LayoutTagHandler extends TagHandler { 074 075 private static final Log log = LogFactory.getLog(LayoutTagHandler.class); 076 077 protected final TagConfig config; 078 079 /** 080 * The layout instance to render, instead of resolving it from a name or definition 081 * 082 * @since 5.7 083 */ 084 protected final TagAttribute layout; 085 086 protected final TagAttribute name; 087 088 /** 089 * @since 5.5. 090 */ 091 protected final TagAttribute category; 092 093 /** 094 * @since 5.4.2 095 */ 096 protected final TagAttribute definition; 097 098 protected final TagAttribute mode; 099 100 protected final TagAttribute value; 101 102 protected final TagAttribute template; 103 104 protected final TagAttribute selectedRows; 105 106 protected final TagAttribute selectedColumns; 107 108 protected final TagAttribute selectAllByDefault; 109 110 /** 111 * Parameter used to specify that layout should not be rendered, only resolved and exposed to the context. 112 * 113 * @since 5.7 114 */ 115 protected final TagAttribute resolveOnly; 116 117 protected final TagAttribute[] vars; 118 119 protected final String[] reservedVarsArray = { "id", "layout", "name", "category", "definition", "mode", "value", 120 "template", "selectedRows", "selectedColumns", "selectAllByDefault", "resolveOnly" }; 121 122 public LayoutTagHandler(TagConfig config) { 123 super(config); 124 this.config = config; 125 name = getAttribute("name"); 126 category = getAttribute("category"); 127 definition = getAttribute("definition"); 128 layout = getAttribute("layout"); 129 if (name == null && definition == null && layout == null) { 130 throw new TagException(this.tag, "At least one of attributes 'name', 'layout' or 'definition'" 131 + " is required"); 132 } 133 mode = getAttribute("mode"); 134 value = getRequiredAttribute("value"); 135 if (layout == null && (name != null || definition != null)) { 136 if (mode == null) { 137 throw new TagException(this.tag, "Attribute 'mode' is required when using attribute" 138 + " 'name' or 'definition' so that the " + "layout instance can be resolved"); 139 } 140 } 141 template = getAttribute("template"); 142 selectedRows = getAttribute("selectedRows"); 143 selectedColumns = getAttribute("selectedColumns"); 144 if (selectedRows != null && selectedColumns != null) { 145 throw new TagException(this.tag, "Attributes 'selectedRows' " 146 + "and 'selectedColumns' are aliases: only one of " + "them should be filled"); 147 } 148 selectAllByDefault = getAttribute("selectAllByDefault"); 149 resolveOnly = getAttribute("resolveOnly"); 150 vars = tag.getAttributes().getAll(); 151 } 152 153 @SuppressWarnings("unchecked") 154 // TODO: add javadoc about variables exposed 155 public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException { 156 WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class); 157 158 // add additional properties put on tag 159 Map<String, Serializable> additionalProps = new HashMap<String, Serializable>(); 160 List<String> reservedVars = Arrays.asList(reservedVarsArray); 161 for (TagAttribute var : vars) { 162 String localName = var.getLocalName(); 163 if (!reservedVars.contains(localName)) { 164 // resolve value as there's no alias value expression exposed 165 // for layout properties 166 additionalProps.put(localName, (Serializable) var.getObject(ctx)); 167 } 168 } 169 170 // expose some layout variables before layout creation so that they 171 // can be used in mode expressions 172 VariableMapper orig = ctx.getVariableMapper(); 173 VariableMapper vm = new VariableMapperWrapper(orig); 174 ctx.setVariableMapper(vm); 175 176 FaceletHandlerHelper helper = new FaceletHandlerHelper(ctx, config); 177 try { 178 Layout layoutInstance = null; 179 180 String valueName = value.getValue(); 181 if (ComponentTagUtils.isStrictValueReference(valueName)) { 182 valueName = ComponentTagUtils.getBareValueName(valueName); 183 } 184 185 String templateValue = null; 186 if (template != null) { 187 templateValue = template.getValue(ctx); 188 } 189 190 boolean resolveOnlyValue = false; 191 if (resolveOnly != null) { 192 resolveOnlyValue = resolveOnly.getBoolean(ctx); 193 } 194 195 if (layout != null) { 196 // resolve layout instance given as attribute 197 layoutInstance = (Layout) layout.getObject(ctx, Layout.class); 198 if (layoutInstance == null) { 199 String errMsg = String.format("Layout not found"); 200 applyErrorHandler(ctx, parent, helper, errMsg); 201 } else { 202 Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, layoutInstance.getMode()); 203 for (Map.Entry<String, ValueExpression> var : vars.entrySet()) { 204 vm.setVariable(var.getKey(), var.getValue()); 205 } 206 layoutInstance.setValueName(valueName); 207 applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue, 208 additionalProps, vars, resolveOnlyValue); 209 } 210 } else { 211 // build layout instance from other attributes 212 String modeValue = mode.getValue(ctx); 213 214 List<String> selectedRowsValue = null; 215 boolean selectAllByDefaultValue = false; 216 217 Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, modeValue); 218 for (Map.Entry<String, ValueExpression> var : vars.entrySet()) { 219 vm.setVariable(var.getKey(), var.getValue()); 220 } 221 222 if (selectedRows != null || selectedColumns != null) { 223 if (selectedRows != null) { 224 selectedRowsValue = (List<String>) selectedRows.getObject(ctx, List.class); 225 } else if (selectedColumns != null) { 226 List<String> selectedColumnsList = (List<String>) selectedColumns.getObject(ctx, List.class); 227 // Handle empty selected columns list as null to 228 // display all columns. 229 if (selectedColumnsList != null && selectedColumnsList.isEmpty()) { 230 selectedColumnsList = null; 231 } 232 selectedRowsValue = selectedColumnsList; 233 } 234 } 235 if (selectAllByDefault != null) { 236 selectAllByDefaultValue = selectAllByDefault.getBoolean(ctx); 237 } 238 239 if (name != null) { 240 String layoutCategory = null; 241 if (category != null) { 242 layoutCategory = category.getValue(ctx); 243 } 244 245 String nameValue = name.getValue(ctx); 246 List<String> layoutNames = resolveLayoutNames(nameValue); 247 for (String layoutName : layoutNames) { 248 layoutInstance = layoutService.getLayout(ctx, layoutName, layoutCategory, modeValue, valueName, 249 selectedRowsValue, selectAllByDefaultValue); 250 if (layoutInstance == null) { 251 String errMsg = String.format("Layout '%s' not found", layoutName); 252 applyErrorHandler(ctx, parent, helper, errMsg); 253 } else { 254 applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue, 255 additionalProps, vars, resolveOnlyValue); 256 } 257 } 258 } 259 260 if (definition != null) { 261 LayoutDefinition layoutDef = (LayoutDefinition) definition.getObject(ctx, LayoutDefinition.class); 262 263 if (layoutDef == null) { 264 String errMsg = "Layout definition resolved to null"; 265 applyErrorHandler(ctx, parent, helper, errMsg); 266 } else { 267 layoutInstance = layoutService.getLayout(ctx, layoutDef, modeValue, valueName, 268 selectedRowsValue, selectAllByDefaultValue); 269 applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue, 270 additionalProps, vars, resolveOnlyValue); 271 } 272 } 273 } 274 275 } finally { 276 // layout resolved => cleanup variable mapper 277 ctx.setVariableMapper(orig); 278 } 279 280 } 281 282 /** 283 * Resolves layouts names, splitting on character "," and trimming resulting names, and allowing empty strings if 284 * the whole string is not empty to ease up rendering of layout names using variables. 285 * <p> 286 * For instance, if value is null or empty, will return a single empty layout name "". If value is "," it will 287 * return an empty list, triggering no error for usage like <nxl:layout name="#{myLayout}, #{myOtherLayout}" [...] 288 * /> 289 */ 290 protected List<String> resolveLayoutNames(String nameValue) { 291 List<String> res = new ArrayList<String>(); 292 if (nameValue != null) { 293 String[] split = nameValue.split(",|\\s"); 294 if (split != null) { 295 for (String item : split) { 296 if (!StringUtils.isBlank(item)) { 297 res.add(item.trim()); 298 } 299 } 300 } 301 } 302 return res; 303 } 304 305 protected void applyLayoutHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper, 306 WebLayoutManager layoutService, Layout layoutInstance, String templateValue, 307 Map<String, Serializable> additionalProps, Map<String, ValueExpression> vars, boolean resolveOnly) 308 throws IOException, FacesException, ELException { 309 310 // set unique id on layout, unless layout is only resolved 311 if (!resolveOnly) { 312 layoutInstance.setId(helper.generateLayoutId(layoutInstance.getName())); 313 } 314 315 // add additional properties put on tag 316 Map<String, Serializable> layoutProps = layoutInstance.getProperties(); 317 if (additionalProps != null && !additionalProps.isEmpty()) { 318 for (Map.Entry<String, Serializable> entry : additionalProps.entrySet()) { 319 // XXX: do not override with empty property values if already 320 // set on the layout properties 321 String key = entry.getKey(); 322 Serializable value = entry.getValue(); 323 if (layoutProps.containsKey(key) 324 && (value == null || ((value instanceof String) && StringUtils.isBlank((String) value)))) { 325 // do not override property on layout 326 if (log.isDebugEnabled()) { 327 log.debug(String.format("Do not override property '%s' with " 328 + "empty value on layout named '%s'", key, layoutInstance.getName())); 329 } 330 } else { 331 layoutInstance.setProperty(key, value); 332 } 333 } 334 } 335 336 if (StringUtils.isBlank(templateValue)) { 337 templateValue = layoutInstance.getTemplate(); 338 } 339 340 // expose layout instance to variable mapper to ensure good 341 // resolution of properties 342 ExpressionFactory eFactory = ctx.getExpressionFactory(); 343 ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class); 344 ctx.getVariableMapper().setVariable(RenderVariables.layoutVariables.layout.name(), layoutVe); 345 346 // expose all variables through an alias tag handler 347 vars.putAll(getVariablesForLayoutRendering(ctx, layoutService, layoutInstance)); 348 349 List<String> blockedPatterns = new ArrayList<String>(); 350 blockedPatterns.add(RenderVariables.layoutVariables.layout.name()); 351 blockedPatterns.add(RenderVariables.layoutVariables.layoutProperty.name() + "_*"); 352 353 final String layoutTagConfigId = layoutInstance.getTagConfigId(); 354 355 if (resolveOnly) { 356 FaceletHandler handler = helper.getAliasTagHandler(layoutTagConfigId, vars, blockedPatterns, nextHandler); 357 // apply 358 handler.apply(ctx, parent); 359 } else { 360 if (!StringUtils.isBlank(templateValue)) { 361 TagAttribute srcAttr = helper.createAttribute("template", templateValue); 362 TagConfig config = TagConfigFactory.createTagConfig(this.config, layoutTagConfigId, 363 FaceletHandlerHelper.getTagAttributes(srcAttr), nextHandler); 364 FaceletHandler includeHandler = new DecorateHandler(config); 365 FaceletHandler handler; 366 if (FaceletHandlerHelper.isDevModeEnabled(ctx)) { 367 // decorate handler with dev handler 368 FaceletHandler devHandler = getDevFaceletHandler(ctx, helper, config, layoutInstance); 369 FaceletHandler nextHandler; 370 if (devHandler == null) { 371 nextHandler = includeHandler; 372 } else { 373 nextHandler = new DevTagHandler(config, layoutInstance.getName(), includeHandler, devHandler); 374 } 375 handler = helper.getAliasTagHandler(layoutTagConfigId, vars, blockedPatterns, nextHandler); 376 } else { 377 handler = helper.getAliasTagHandler(layoutTagConfigId, vars, blockedPatterns, includeHandler); 378 } 379 // apply 380 handler.apply(ctx, parent); 381 } else { 382 String errMsg = String.format("Missing template property for layout '%s'", layoutInstance.getName()); 383 applyErrorHandler(ctx, parent, helper, errMsg); 384 } 385 } 386 } 387 388 protected Map<String, ValueExpression> getVariablesForLayoutBuild(FaceletContext ctx, String modeValue) { 389 Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>(); 390 ValueExpression valueExpr = value.getValueExpression(ctx, Object.class); 391 vars.put(RenderVariables.globalVariables.value.name(), valueExpr); 392 // vars.put(RenderVariables.globalVariables.document.name(), 393 // valueExpr); 394 vars.put(RenderVariables.globalVariables.layoutValue.name(), valueExpr); 395 ExpressionFactory eFactory = ctx.getExpressionFactory(); 396 ValueExpression modeVe = eFactory.createValueExpression(modeValue, String.class); 397 vars.put(RenderVariables.globalVariables.layoutMode.name(), modeVe); 398 // mode as alias to layoutMode 399 vars.put(RenderVariables.globalVariables.mode.name(), modeVe); 400 return vars; 401 } 402 403 /** 404 * Computes variables for rendering, making available the layout instance and its properties to the context. 405 */ 406 protected Map<String, ValueExpression> getVariablesForLayoutRendering(FaceletContext ctx, 407 WebLayoutManager layoutService, Layout layoutInstance) { 408 Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>(); 409 ExpressionFactory eFactory = ctx.getExpressionFactory(); 410 411 // expose layout value 412 ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class); 413 vars.put(RenderVariables.layoutVariables.layout.name(), layoutVe); 414 415 // expose layout properties too 416 for (Map.Entry<String, Serializable> prop : layoutInstance.getProperties().entrySet()) { 417 String key = prop.getKey(); 418 String name = String.format("%s_%s", RenderVariables.layoutVariables.layoutProperty.name(), key); 419 String value; 420 Serializable valueInstance = prop.getValue(); 421 if (!layoutService.referencePropertyAsExpression(key, valueInstance, null, null, null, null)) { 422 // FIXME: this will not be updated correctly using ajax 423 value = (String) valueInstance; 424 } else { 425 // create a reference so that it's a real expression and it's 426 // not kept (cached) in a component value on ajax refresh 427 value = String.format("#{%s.properties.%s}", RenderVariables.layoutVariables.layout.name(), key); 428 } 429 vars.put(name, eFactory.createValueExpression(ctx, value, Object.class)); 430 } 431 432 return vars; 433 } 434 435 protected void applyErrorHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper, String message) 436 throws IOException { 437 log.error(message); 438 ComponentHandler output = helper.getErrorComponentHandler(null, message); 439 output.apply(ctx, parent); 440 } 441 442 protected FaceletHandler getDevFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, TagConfig config, 443 Layout layout) { 444 if (StringUtils.isBlank(layout.getDevTemplate())) { 445 return null; 446 } 447 // use the default dev handler for widget types 448 TagAttribute attr = helper.createAttribute("layout", 449 String.format("#{%s}", RenderVariables.layoutVariables.layout.name())); 450 TagAttributes devWidgetAttributes = FaceletHandlerHelper.getTagAttributes(attr); 451 TagConfig devWidgetConfig = TagConfigFactory.createTagConfig(config, layout.getTagConfigId(), 452 devWidgetAttributes, new org.nuxeo.ecm.platform.ui.web.tag.handler.LeafFaceletHandler()); 453 return new LayoutDevTagHandler(devWidgetConfig); 454 } 455}