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}