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