001/*
002 * (C) Copyright 2011 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.ui.web.component.holder;
020
021import java.io.IOException;
022import java.util.List;
023
024import javax.el.ELException;
025import javax.el.ValueExpression;
026import javax.faces.FacesException;
027import javax.faces.component.ContextCallback;
028import javax.faces.component.UIComponent;
029import javax.faces.component.UIInput;
030import javax.faces.component.html.HtmlInputText;
031import javax.faces.component.visit.VisitCallback;
032import javax.faces.component.visit.VisitContext;
033import javax.faces.context.FacesContext;
034import javax.faces.event.FacesEvent;
035import javax.faces.event.PhaseId;
036
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.ecm.platform.ui.web.binding.alias.AliasEvent;
040import org.nuxeo.ecm.platform.ui.web.binding.alias.AliasVariableMapper;
041import org.nuxeo.ecm.platform.ui.web.component.ResettableComponent;
042
043import com.sun.faces.facelets.tag.jsf.ComponentSupport;
044
045/**
046 * Component that keeps and exposes a value to the context during each JSF phase.
047 * <p>
048 * Can be bound to a value as an input component, or not submit the value and still expose it to the context at build
049 * time as well as at render time.
050 *
051 * @since 5.5
052 */
053public class UIValueHolder extends HtmlInputText implements ResettableComponent {
054
055    private static final Log log = LogFactory.getLog(UIValueHolder.class);
056
057    public static final String COMPONENT_TYPE = UIValueHolder.class.getName();
058
059    public static final String COMPONENT_FAMILY = UIInput.COMPONENT_FAMILY;
060
061    protected String var;
062
063    /**
064     * <p>
065     * The submittedValue value of this {@link UIInput} component.
066     * </p>
067     */
068    protected transient Object submittedValue = null;
069
070    protected Boolean submitValue;
071
072    @Override
073    public String getFamily() {
074        return COMPONENT_FAMILY;
075    }
076
077    @Override
078    public String getRendererType() {
079        return COMPONENT_TYPE;
080    }
081
082    @Override
083    public boolean getRendersChildren() {
084        return true;
085    }
086
087    @Override
088    public void broadcast(FacesEvent event) {
089        if (event instanceof AliasEvent) {
090            FacesContext context = getFacesContext();
091            AliasVariableMapper alias = getAliasVariableMapper(context);
092            try {
093                AliasVariableMapper.exposeAliasesToRequest(context, alias);
094                FacesEvent origEvent = ((AliasEvent) event).getOriginalEvent();
095                origEvent.getComponent().broadcast(origEvent);
096            } finally {
097                if (alias != null) {
098                    AliasVariableMapper.removeAliasesExposedToRequest(context, alias.getId());
099                }
100            }
101        } else {
102            super.broadcast(event);
103        }
104    }
105
106    @Override
107    public void queueEvent(FacesEvent event) {
108        event = new AliasEvent(this, event);
109        super.queueEvent(event);
110    }
111
112    @Override
113    public boolean invokeOnComponent(FacesContext context, String clientId, ContextCallback callback)
114            throws FacesException {
115        AliasVariableMapper alias = getAliasVariableMapper(context);
116        try {
117            AliasVariableMapper.exposeAliasesToRequest(context, alias);
118            return super.invokeOnComponent(context, clientId, callback);
119        } finally {
120            if (alias != null) {
121                AliasVariableMapper.removeAliasesExposedToRequest(context, alias.getId());
122            }
123        }
124    }
125
126    @Override
127    public void encodeBegin(FacesContext context) throws IOException {
128        AliasVariableMapper alias = getAliasVariableMapper(context);
129        AliasVariableMapper.exposeAliasesToRequest(context, alias);
130        super.encodeBegin(context);
131    }
132
133    @Override
134    public void encodeChildren(final FacesContext context) throws IOException {
135        // no need to expose variables: already done in #encodeBegin
136        processFacetsAndChildren(context, PhaseId.RENDER_RESPONSE);
137    }
138
139    @Override
140    public void encodeEnd(FacesContext context) throws IOException {
141        super.encodeEnd(context);
142        AliasVariableMapper alias = getAliasVariableMapper(context);
143        if (alias != null) {
144            AliasVariableMapper.removeAliasesExposedToRequest(context, alias.getId());
145        }
146    }
147
148    @Override
149    public void processDecodes(FacesContext context) {
150        if (context == null) {
151            throw new NullPointerException();
152        }
153
154        // Skip processing if our rendered flag is false
155        if (!isRendered()) {
156            return;
157        }
158
159        // XXX: decode component itself first, so that potential submitted
160        // value is accurately exposed in context for facets and children
161        try {
162            decode(context);
163        } catch (RuntimeException e) {
164            context.renderResponse();
165            throw e;
166        }
167
168        processFacetsAndChildrenWithVariable(context, PhaseId.APPLY_REQUEST_VALUES);
169
170        if (isImmediate()) {
171            executeValidate(context);
172        }
173    }
174
175    @Override
176    public void processValidators(FacesContext context) {
177        if (context == null) {
178            throw new NullPointerException();
179        }
180
181        // Skip processing if our rendered flag is false
182        if (!isRendered()) {
183            return;
184        }
185
186        processFacetsAndChildrenWithVariable(context, PhaseId.PROCESS_VALIDATIONS);
187
188        if (!isImmediate()) {
189            executeValidate(context);
190        }
191    }
192
193    /**
194     * Executes validation logic.
195     */
196    private void executeValidate(FacesContext context) {
197        try {
198            validate(context);
199        } catch (RuntimeException e) {
200            context.renderResponse();
201            throw e;
202        }
203
204        if (!isValid()) {
205            context.renderResponse();
206        }
207    }
208
209    @Override
210    public void processUpdates(FacesContext context) {
211        if (context == null) {
212            throw new NullPointerException();
213        }
214
215        // Skip processing if our rendered flag is false
216        if (!isRendered()) {
217            return;
218        }
219
220        processFacetsAndChildrenWithVariable(context, PhaseId.UPDATE_MODEL_VALUES);
221
222        if (Boolean.TRUE.equals(getSubmitValue())) {
223            try {
224                updateModel(context);
225            } catch (RuntimeException e) {
226                context.renderResponse();
227                throw e;
228            }
229        }
230
231        if (!isValid()) {
232            context.renderResponse();
233        }
234    }
235
236    protected final void processFacetsAndChildren(final FacesContext context, final PhaseId phaseId) {
237        List<UIComponent> stamps = getChildren();
238        for (UIComponent stamp : stamps) {
239            processComponent(context, stamp, phaseId);
240        }
241    }
242
243    protected final void processFacetsAndChildrenWithVariable(final FacesContext context, final PhaseId phaseId) {
244        AliasVariableMapper alias = getAliasVariableMapper(context);
245        try {
246            AliasVariableMapper.exposeAliasesToRequest(context, alias);
247            processFacetsAndChildren(context, phaseId);
248        } finally {
249            if (alias != null) {
250                AliasVariableMapper.removeAliasesExposedToRequest(context, alias.getId());
251            }
252        }
253    }
254
255    protected final void processComponent(FacesContext context, UIComponent component, PhaseId phaseId) {
256        if (component != null) {
257            if (phaseId == PhaseId.APPLY_REQUEST_VALUES) {
258                component.processDecodes(context);
259            } else if (phaseId == PhaseId.PROCESS_VALIDATIONS) {
260                component.processValidators(context);
261            } else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) {
262                component.processUpdates(context);
263            } else if (phaseId == PhaseId.RENDER_RESPONSE) {
264                try {
265                    ComponentSupport.encodeRecursive(context, component);
266                } catch (IOException err) {
267                    log.error("Error while rendering component " + component);
268                }
269            } else {
270                throw new IllegalArgumentException("Bad PhaseId:" + phaseId);
271            }
272        }
273    }
274
275    // properties management
276
277    public String getVar() {
278        if (var != null) {
279            return var;
280        }
281        ValueExpression ve = getValueExpression("var");
282        if (ve != null) {
283            try {
284                return (String) ve.getValue(getFacesContext().getELContext());
285            } catch (ELException e) {
286                throw new FacesException(e);
287            }
288        } else {
289            return null;
290        }
291    }
292
293    public void setVar(String var) {
294        this.var = var;
295    }
296
297    public Boolean getSubmitValue() {
298        if (submitValue != null) {
299            return submitValue;
300        }
301        ValueExpression ve = getValueExpression("submitValue");
302        if (ve != null) {
303            try {
304                return Boolean.valueOf(Boolean.TRUE.equals(ve.getValue(getFacesContext().getELContext())));
305            } catch (ELException e) {
306                throw new FacesException(e);
307            }
308        } else {
309            return Boolean.TRUE;
310        }
311    }
312
313    public void setSubmitValue(Boolean submitValue) {
314        this.submitValue = submitValue;
315    }
316
317    public Object getValueToExpose() {
318        Object value = getSubmittedValue();
319        if (value == null) {
320            // get original value bound
321            value = super.getValue();
322        }
323        return value;
324    }
325
326    protected AliasVariableMapper getAliasVariableMapper(FacesContext ctx) {
327        String var = getVar();
328        Object value = getValueToExpose();
329        AliasVariableMapper alias = new AliasVariableMapper();
330        // reuse facelets id set on component
331        String aliasId = getFaceletId();
332        alias.setId(aliasId);
333        alias.setVariable(var, ctx.getApplication().getExpressionFactory().createValueExpression(value, Object.class));
334        return alias;
335    }
336
337    // state holder
338
339    @Override
340    public void restoreState(FacesContext context, Object state) {
341        Object[] values = (Object[]) state;
342        super.restoreState(context, values[0]);
343        var = (String) values[1];
344        submitValue = (Boolean) values[2];
345        submittedValue = values[3];
346    }
347
348    /**
349     * Saves the locally set literal values kept on the component (from standard tags attributes) and since 5.6, also
350     * saves the submitted value as {@link UIInput#saveState(FacesContext)} does not do it (see NXP-8898).
351     */
352    @Override
353    public Object saveState(FacesContext context) {
354        return new Object[] { super.saveState(context), var, submitValue, getSubmittedValue() };
355    }
356
357    /**
358     * Resets the value holder local values
359     *
360     * @since 5.7
361     */
362    @Override
363    public void resetCachedModel() {
364        if (getValueExpression("value") != null) {
365            setValue(null);
366            setLocalValueSet(false);
367        }
368        setSubmittedValue(null);
369    }
370
371    @Override
372    public boolean visitTree(VisitContext visitContext, VisitCallback callback) {
373        if (!isVisitable(visitContext)) {
374            return false;
375        }
376        FacesContext facesContext = visitContext.getFacesContext();
377        AliasVariableMapper alias = getAliasVariableMapper(facesContext);
378        try {
379            AliasVariableMapper.exposeAliasesToRequest(facesContext, alias);
380            return super.visitTree(visitContext, callback);
381        } finally {
382            if (alias != null) {
383                AliasVariableMapper.removeAliasesExposedToRequest(facesContext, alias.getId());
384            }
385        }
386    }
387
388    public String getFaceletId() {
389        return (String) getAttributes().get(ComponentSupport.MARK_CREATED);
390    }
391
392    public NuxeoValueHolderBean lookupBean(FacesContext ctx) {
393        String expr = "#{" + NuxeoValueHolderBean.NAME + "}";
394        NuxeoValueHolderBean bean = (NuxeoValueHolderBean) ctx.getApplication().evaluateExpressionGet(ctx, expr,
395                Object.class);
396        if (bean == null) {
397            log.error("Managed bean not found: " + expr);
398            return null;
399        }
400        return bean;
401    }
402
403    protected void saveToBean(Object value) {
404        if (getFaceletId() == null) {
405            // not added to the view yet, do not bother
406            return;
407        }
408        FacesContext ctx = FacesContext.getCurrentInstance();
409        if (ctx != null) {
410            NuxeoValueHolderBean bean = lookupBean(ctx);
411            if (bean != null) {
412                bean.saveState(this, value);
413            }
414        }
415    }
416
417    @Override
418    public void setSubmittedValue(Object submittedValue) {
419        this.submittedValue = submittedValue;
420        saveToBean(submittedValue);
421    }
422
423    @Override
424    public Object getSubmittedValue() {
425        return submittedValue;
426    }
427
428    @Override
429    public void setValue(Object value) {
430        super.setValue(value);
431        saveToBean(value);
432    }
433
434}