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