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.lang3.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}