001/* 002 * (C) Copyright 2016 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 * Anahide Tchertchian 018 */ 019package org.nuxeo.ecm.platform.actions.facelets; 020 021import java.io.IOException; 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import javax.el.ELException; 030import javax.el.ExpressionFactory; 031import javax.el.ValueExpression; 032import javax.el.VariableMapper; 033import javax.faces.FacesException; 034import javax.faces.component.UIComponent; 035import javax.faces.view.facelets.FaceletContext; 036import javax.faces.view.facelets.FaceletHandler; 037import javax.faces.view.facelets.MetaRuleset; 038import javax.faces.view.facelets.MetaTagHandler; 039import javax.faces.view.facelets.TagAttribute; 040import javax.faces.view.facelets.TagAttributes; 041import javax.faces.view.facelets.TagConfig; 042 043import org.apache.commons.lang.StringUtils; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.platform.actions.Action; 048import org.nuxeo.ecm.platform.forms.layout.api.BuiltinWidgetModes; 049import org.nuxeo.ecm.platform.forms.layout.api.Widget; 050import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetDefinitionImpl; 051import org.nuxeo.ecm.platform.forms.layout.facelets.FaceletHandlerHelper; 052import org.nuxeo.ecm.platform.forms.layout.facelets.RenderVariables; 053import org.nuxeo.ecm.platform.forms.layout.facelets.WidgetTagHandler; 054import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager; 055import org.nuxeo.ecm.platform.ui.web.binding.BlockingVariableMapper; 056import org.nuxeo.ecm.platform.ui.web.tag.handler.FormTagHandler; 057import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory; 058import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils; 059import org.nuxeo.ecm.platform.ui.web.util.FaceletDebugTracer; 060import org.nuxeo.runtime.api.Framework; 061 062import com.sun.faces.facelets.tag.TagAttributesImpl; 063 064/** 065 * Tag handler rendering an action given its type, applying corresponding widget tag handler, axposing additional 066 * variables for action templates usage. 067 * 068 * @since 8.2 069 */ 070public class ActionTagHandler extends MetaTagHandler { 071 072 private static final Log log = LogFactory.getLog(ActionTagHandler.class); 073 074 protected final TagConfig config; 075 076 protected final TagAttribute action; 077 078 protected final TagAttribute widgetName; 079 080 protected final TagAttribute value; 081 082 protected final TagAttribute mode; 083 084 protected final TagAttribute addForm; 085 086 protected final TagAttribute useAjaxForm; 087 088 protected final TagAttribute formStyleClass; 089 090 protected final TagAttribute postFilterMethod; 091 092 protected final TagAttribute[] vars; 093 094 protected final String[] reservedVarsArray = { "action", "widgetName", "value", "mode", "addForm", "useAjaxForm", 095 "formStyleClass", "postFilterMethod" }; 096 097 public ActionTagHandler(TagConfig config) { 098 super(config); 099 this.config = config; 100 101 action = getRequiredAttribute("action"); 102 widgetName = getAttribute("widgetName"); 103 value = getRequiredAttribute("value"); 104 mode = getAttribute("mode"); 105 addForm = getAttribute("addForm"); 106 useAjaxForm = getAttribute("useAjaxForm"); 107 formStyleClass = getAttribute("formStyleClass"); 108 postFilterMethod = getAttribute("postFilterMethod"); 109 110 vars = tag.getAttributes().getAll(); 111 } 112 113 /** 114 * Renders given widget resolving its {@link FaceletHandler} from {@link WebLayoutManager} configuration. 115 * <p> 116 * Variables exposed: {@link RenderVariables.globalVariables#value}, same variable suffixed with "_n" where n is the 117 * widget level, and {@link RenderVariables.globalVariables#document}. 118 */ 119 public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException { 120 long start = FaceletDebugTracer.start(); 121 Action actionInstance = null; 122 123 try { 124 if (action != null) { 125 actionInstance = (Action) action.getObject(ctx, Action.class); 126 } 127 if (actionInstance == null) { 128 return; 129 } 130 131 VariableMapper orig = ctx.getVariableMapper(); 132 try { 133 BlockingVariableMapper vm = new BlockingVariableMapper(orig); 134 ctx.setVariableMapper(vm); 135 136 // build corresponding widget and adjust properties 137 String wtype = actionInstance.getType(); 138 if (StringUtils.isBlank(wtype)) { 139 wtype = "link"; 140 } 141 String wcat = "jsfAction"; 142 143 String modeValue = null; 144 if (mode != null) { 145 modeValue = mode.getValue(ctx); 146 } 147 if (StringUtils.isBlank(modeValue)) { 148 modeValue = BuiltinWidgetModes.VIEW; 149 } 150 151 Map<String, Serializable> props = new HashMap<>(); 152 // put all action properties 153 props.putAll(actionInstance.getProperties()); 154 if ("template".equals(wtype)) { 155 // avoid erasing template value from widget type configuration, and match template 156 String templateName = "template"; 157 String modeTemplateName = "template" + "_" + modeValue; 158 if (BuiltinWidgetModes.VIEW.equals(modeValue) && props.containsKey(templateName)) { 159 props.put("action_template", props.get(templateName)); 160 } else if (props.containsKey(modeTemplateName)) { 161 props.put("action_template", props.get(modeTemplateName)); 162 props.remove(modeTemplateName); 163 } 164 props.remove(templateName); 165 } 166 // handle onclick 167 StringBuilder fullOnclick = new StringBuilder(); 168 if (BuiltinWidgetModes.VIEW.equals(modeValue) && props.containsKey("confirmMessage")) { 169 String confirmMessage = (String) props.get("confirmMessage"); 170 if (!StringUtils.isEmpty(confirmMessage)) { 171 fullOnclick.append( 172 "var message = \"#{nxu:translate(widgetProperty_confirmMessage, widgetProperty_confirmMessageArgs)}\";if (message != \"\" && !confirm(message)) {return false;};"); 173 } 174 } 175 String confirm = actionInstance.getConfirm(); 176 if (!StringUtils.isEmpty(confirm)) { 177 fullOnclick.append(confirm).append(";"); 178 } 179 String onclick = (String) actionInstance.getProperties().get("onclick"); 180 if (!StringUtils.isEmpty(onclick)) { 181 fullOnclick.append(onclick).append(";"); 182 } 183 props.put("immediate", actionInstance.isImmediate()); 184 props.put("icon", actionInstance.getIcon()); 185 props.put("onclick", actionInstance.getConfirm()); 186 props.put("accessKey", actionInstance.getAccessKey()); 187 props.put("link", actionInstance.getLink()); 188 props.put("actionId", actionInstance.getId()); 189 props.put("action", actionInstance); 190 if (useAjaxForm != null && !props.containsKey("useAjaxForm")) { 191 props.put("useAjaxForm", useAjaxForm.getValue()); 192 } 193 194 String valueName = value.getValue(); 195 String bareValueName = valueName; 196 if (ComponentTagUtils.isStrictValueReference(valueName)) { 197 bareValueName = ComponentTagUtils.getBareValueName(valueName); 198 } 199 200 // add filtering method if needed 201 if (!actionInstance.isFiltered()) { 202 // make sure variables are in the context for this filter resolution 203 ExpressionFactory eFactory = ctx.getExpressionFactory(); 204 ValueExpression actionVe = eFactory.createValueExpression(actionInstance, Action.class); 205 vm.setVariable("action", actionVe); 206 vm.addBlockedPattern("action"); 207 208 String bindingValue = bareValueName; 209 boolean bindingDone = false; 210 if (props.containsKey("actionContextDocument")) { 211 Object val = props.get("actionContextDocument"); 212 if (val instanceof String && ComponentTagUtils.isStrictValueReference((String) val)) { 213 bindingValue = ComponentTagUtils.getBareValueName((String) val); 214 ValueExpression bindingVe = eFactory.createValueExpression(ctx, (String) val, Object.class); 215 vm.setVariable("actionContextDocument", bindingVe); 216 vm.addBlockedPattern("actionContextDocument"); 217 bindingDone = true; 218 } 219 } 220 if (!bindingDone) { 221 // just bound current value to make expressions consistent 222 vm.setVariable("actionContextDocument", value.getValueExpression(ctx, DocumentModel.class)); 223 vm.addBlockedPattern("actionContextDocument"); 224 } 225 226 String method = null; 227 if (postFilterMethod != null) { 228 method = postFilterMethod.getValue(ctx); 229 } 230 if (StringUtils.isBlank(method)) { 231 method = "webActions.isAvailableForDocument"; 232 } 233 String filterExpr = "#{" + method + "(" + bindingValue + ", action)}"; 234 props.put("available", filterExpr); 235 props.put("enabled", filterExpr); 236 } else { 237 props.put("available", actionInstance.getAvailable()); 238 props.put("enabled", "true"); 239 } 240 241 // add all extra props passed to the tag 242 String widgetPropertyMarker = RenderVariables.widgetVariables.widgetProperty.name() + "_"; 243 List<String> reservedVars = Arrays.asList(reservedVarsArray); 244 for (TagAttribute var : vars) { 245 String localName = var.getLocalName(); 246 if (!reservedVars.contains(localName)) { 247 if (localName != null && localName.startsWith(widgetPropertyMarker)) { 248 localName = localName.substring(widgetPropertyMarker.length()); 249 } 250 props.put(localName, var.getValue()); 251 } 252 } 253 254 String widgetNameValue = null; 255 if (widgetName != null) { 256 widgetNameValue = widgetName.getValue(ctx); 257 } 258 if (StringUtils.isBlank(widgetNameValue)) { 259 widgetNameValue = actionInstance.getId(); 260 } 261 // avoid double markers 262 if (widgetNameValue != null && widgetNameValue.startsWith(FaceletHandlerHelper.WIDGET_ID_PREFIX)) { 263 widgetNameValue = widgetNameValue.substring(FaceletHandlerHelper.WIDGET_ID_PREFIX.length()); 264 } 265 266 WidgetDefinitionImpl wDef = new WidgetDefinitionImpl(widgetNameValue, wtype, actionInstance.getLabel(), 267 actionInstance.getHelp(), true, null, null, props, null); 268 wDef.setTypeCategory(wcat); 269 wDef.setDynamic(true); 270 WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class); 271 Widget widgetInstance = layoutService.createWidget(ctx, wDef, modeValue, bareValueName, null); 272 if (widgetInstance == null) { 273 return; 274 } 275 // set unique id on widget before exposing it to the context 276 FaceletHandlerHelper helper = new FaceletHandlerHelper(config); 277 WidgetTagHandler.generateWidgetId(ctx, helper, widgetInstance, false); 278 279 // expose widget variables 280 WidgetTagHandler.exposeWidgetVariables(ctx, vm, widgetInstance, null, false); 281 282 // create widget handler 283 TagAttributes wattrs = FaceletHandlerHelper.getTagAttributes(); 284 wattrs = FaceletHandlerHelper.addTagAttribute(wattrs, 285 helper.createAttribute(RenderVariables.widgetVariables.widget.name(), 286 "#{" + RenderVariables.widgetVariables.widget.name() + "}")); 287 wattrs = FaceletHandlerHelper.addTagAttribute(wattrs, 288 helper.createAttribute("value", value.getValue())); 289 TagConfig wconfig = TagConfigFactory.createTagConfig(config, config.getTagId(), wattrs, nextHandler); 290 FaceletHandler handler = new WidgetTagHandler(wconfig); 291 292 // expose ajax render props to the context 293 String reRender = (String) props.get("ajaxReRender"); 294 if (!StringUtils.isEmpty(reRender)) { 295 ExpressionFactory eFactory = ctx.getExpressionFactory(); 296 ValueExpression ve = eFactory.createValueExpression( 297 "#{nxu:joinRender(ajaxReRender, " + reRender + ")}", String.class); 298 vm.setVariable("ajaxReRender", ve); 299 } 300 301 // create form handler if needed 302 boolean doAddForm = false; 303 if (addForm != null) { 304 doAddForm = addForm.getBoolean(ctx); 305 } 306 if (!doAddForm) { 307 // check if addForm information held by the action configuration 308 doAddForm = helper.createAttribute("addForm", String.valueOf(widgetInstance.getProperty("addForm"))) 309 .getBoolean(ctx); 310 } 311 if (doAddForm) { 312 // resolve form related attributes early 313 boolean discard = helper.createAttribute("discardSurroundingForm", 314 String.valueOf(widgetInstance.getProperty("discardSurroundingForm"))).getBoolean(ctx); 315 boolean doUseAjaxForm = helper.createAttribute("useAjaxForm", 316 String.valueOf(widgetInstance.getProperty("useAjaxForm"))).getBoolean(ctx); 317 if (!discard || doUseAjaxForm) { 318 List<TagAttribute> fattrs = new ArrayList<>(); 319 if (doUseAjaxForm) { 320 Object ajaxProp = widgetInstance.getProperty("ajaxSupport"); 321 if (ajaxProp == null) { 322 ajaxProp = widgetInstance.getProperty("supportAjax"); 323 } 324 fattrs.add(helper.createAttribute("useAjaxForm", String.valueOf(ajaxProp))); 325 } 326 fattrs.add(helper.createAttribute("disableMultipartForm", 327 String.valueOf(widgetInstance.getProperty("disableMultipartForm")))); 328 fattrs.add(helper.createAttribute("disableDoubleClickShield", 329 String.valueOf(widgetInstance.getProperty("disableDoubleClickShield")))); 330 fattrs.add(helper.createAttribute("styleClass", 331 formStyleClass != null ? formStyleClass.getValue() : null)); 332 fattrs.add(helper.createAttribute("id", widgetInstance.getId() + "_form")); 333 334 TagConfig fconfig = TagConfigFactory.createTagConfig(config, config.getTagId(), 335 new TagAttributesImpl(fattrs.toArray(new TagAttribute[] {})), handler); 336 handler = new FormTagHandler(fconfig); 337 } 338 } 339 340 handler.apply(ctx, parent); 341 342 } finally { 343 ctx.setVariableMapper(orig); 344 } 345 346 } finally { 347 FaceletDebugTracer.trace(start, config.getTag(), actionInstance == null ? null : actionInstance.getId()); 348 } 349 } 350 351 @Override 352 @SuppressWarnings("rawtypes") 353 protected MetaRuleset createMetaRuleset(Class type) { 354 return null; 355 } 356 357}