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}