001// Copyright 2007-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.corelib.components; 016 017import org.apache.tapestry5.*; 018import org.apache.tapestry5.annotations.Environmental; 019import org.apache.tapestry5.annotations.Parameter; 020import org.apache.tapestry5.beaneditor.BeanModel; 021import org.apache.tapestry5.beaneditor.PropertyModel; 022import org.apache.tapestry5.internal.BeanValidationContext; 023import org.apache.tapestry5.ioc.Messages; 024import org.apache.tapestry5.ioc.annotations.Inject; 025import org.apache.tapestry5.ioc.internal.util.TapestryException; 026import org.apache.tapestry5.services.*; 027 028import java.lang.annotation.Annotation; 029import java.util.Locale; 030 031/** 032 * Used to edit a single property of a bean. This is used primarily by {@link BeanEditForm}. Unlike BeanEditForm, the 033 * object to be edited must already exist and the {@linkplain BeanModel model} must be passed in explicitly. 034 * 035 * @tapestrydoc 036 */ 037public class PropertyEditor 038{ 039 /** 040 * Configures and stores a {@link PropertyEditContext} into the {@link Environment}. 041 */ 042 static class SetupEnvironment implements ComponentAction<PropertyEditor> 043 { 044 private static final long serialVersionUID = 5337049721509981997L; 045 046 private final String property; 047 048 public SetupEnvironment(String property) 049 { 050 this.property = property; 051 } 052 053 public void execute(PropertyEditor component) 054 { 055 component.setupEnvironment(property); 056 } 057 058 @Override 059 public String toString() 060 { 061 return String.format("PropertyEditor.SetupEnvironment[%s]", property); 062 } 063 } 064 065 static class CleanupEnvironment implements ComponentAction<PropertyEditor> 066 { 067 private static final long serialVersionUID = 7878694042753046523L; 068 069 public void execute(PropertyEditor component) 070 { 071 component.cleanupEnvironment(); 072 } 073 074 @Override 075 public String toString() 076 { 077 return "PropertyEditor.CleanupEnvironment"; 078 } 079 } 080 081 private static final ComponentAction CLEANUP_ENVIRONMENT = new CleanupEnvironment(); 082 083 /** 084 * The object to be edited by the BeanEditor. This will be read when the component renders and updated when the form 085 * for the component is submitted. Typically, the container will listen for a "prepare" event, in order to ensure 086 * that a non-null value is ready to be read or updated. 087 */ 088 @Parameter(required = true, allowNull = false) 089 private Object object; 090 091 /** 092 * Where to search for local overrides of property editing blocks as block parameters. This is normally the 093 * containing component of the PropertyEditor, but when the component is used within a BeanEditor, it will be the 094 * BeanEditor's block parameters that will be searched. 095 */ 096 @Parameter(value = "this", allowNull = false) 097 private PropertyOverrides overrides; 098 099 /** 100 * Identifies the property to be edited by the editor. 101 */ 102 @Parameter(required = true) 103 private String property; 104 105 /** 106 * The model that identifies the parameters to be edited, their order, and every other aspect. 107 */ 108 @Parameter(required = true, allowNull = false) 109 private BeanModel model; 110 111 @Inject 112 private FieldValidatorDefaultSource fieldValidatorDefaultSource; 113 114 @Inject 115 private Environment environment; 116 117 /** 118 * Source for property editor blocks. This defaults to the default implementation of {@link 119 * org.apache.tapestry5.services.BeanBlockSource}. 120 */ 121 @Parameter(required = true, allowNull = false) 122 private BeanBlockSource beanBlockSource; 123 124 @Inject 125 @Core 126 private BeanBlockSource defaultBeanBlockSource; 127 128 @Inject 129 private Messages messages; 130 131 @Inject 132 private Locale locale; 133 134 @Inject 135 private ComponentResources resources; 136 137 @Inject 138 private FieldTranslatorSource fieldTranslatorSource; 139 140 @Environmental 141 private FormSupport formSupport; 142 143 @Inject 144 private Heartbeat heartbeat; 145 146 private PropertyModel propertyModel; 147 148 BeanBlockSource defaultBeanBlockSource() 149 { 150 return defaultBeanBlockSource; 151 } 152 153 /** 154 * Creates a {@link org.apache.tapestry5.services.PropertyEditContext} and pushes it onto the {@link 155 * org.apache.tapestry5.services.Environment} stack. 156 */ 157 void setupEnvironment(final String propertyName) 158 { 159 propertyModel = model.get(propertyName); 160 161 PropertyEditContext context = new PropertyEditContext() 162 { 163 public Messages getContainerMessages() 164 { 165 return overrides.getOverrideMessages(); 166 } 167 168 public String getLabel() 169 { 170 return propertyModel.getLabel(); 171 } 172 173 public String getPropertyId() 174 { 175 return propertyModel.getId(); 176 } 177 178 public Class getPropertyType() 179 { 180 return propertyModel.getPropertyType(); 181 } 182 183 public Object getPropertyValue() 184 { 185 return propertyModel.getConduit().get(object); 186 } 187 188 public FieldTranslator getTranslator(Field field) 189 { 190 return fieldTranslatorSource.createDefaultTranslator(field, propertyName, 191 overrides.getOverrideMessages(), locale, 192 propertyModel.getPropertyType(), 193 propertyModel.getConduit()); 194 } 195 196 public FieldValidator getValidator(Field field) 197 { 198 return fieldValidatorDefaultSource.createDefaultValidator(field, propertyName, 199 overrides.getOverrideMessages(), locale, 200 propertyModel.getPropertyType(), 201 propertyModel.getConduit()); 202 } 203 204 public void setPropertyValue(Object value) 205 { 206 propertyModel.getConduit().set(object, value); 207 } 208 209 public <T extends Annotation> T getAnnotation(Class<T> annotationClass) 210 { 211 return propertyModel.getAnnotation(annotationClass); 212 } 213 }; 214 215 environment.push(PropertyEditContext.class, context); 216 217 BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class); 218 219 if(beanValidationContext != null) 220 { 221 beanValidationContext.setCurrentProperty(propertyName); 222 } 223 224 heartbeat.begin(); 225 } 226 227 /** 228 * Called at the end of the form render (or at the end of the form submission) to clean up the {@link Environment} 229 * stack. 230 */ 231 void cleanupEnvironment() 232 { 233 heartbeat.end(); 234 235 environment.pop(PropertyEditContext.class); 236 } 237 238 /** 239 * Record into the Form what's needed to process the property. 240 */ 241 void setupRender() 242 { 243 // Sets up the PropertyEditContext for the duration of the render of this component 244 // (which will include the duration of the editor block). 245 246 formSupport.storeAndExecute(this, new SetupEnvironment(property)); 247 } 248 249 /** 250 * Records into the Form the cleanup logic for the property. 251 */ 252 void cleanupRender() 253 { 254 // Removes the PropertyEditContext after this component (including the editor block) 255 // has rendered. 256 257 formSupport.storeAndExecute(this, CLEANUP_ENVIRONMENT); 258 } 259 260 /** 261 * Returns a Block for rendering the property. The Block will be able to access the {@link PropertyEditContext} via 262 * the {@link Environmental} annotation. 263 */ 264 Block beginRender() 265 { 266 267 Block override = overrides.getOverrideBlock(propertyModel.getId()); 268 269 if (override != null) 270 { 271 return override; 272 } 273 274 String dataType = propertyModel.getDataType(); 275 276 if (dataType == null) 277 throw new RuntimeException( 278 String.format("The data type for property '%s' of %s is null.", propertyModel.getPropertyName(), 279 object)); 280 281 try 282 { 283 284 return beanBlockSource.getEditBlock(dataType); 285 } 286 catch (RuntimeException ex) 287 { 288 String message = messages.format("core-block-error", propertyModel.getPropertyName(), dataType, object, ex); 289 290 throw new TapestryException(message, resources.getLocation(), ex); 291 } 292 293 } 294 295 /** 296 * Returns false, to prevent the rendering of the body of the component. PropertyEditor should not have a body. 297 */ 298 boolean beforeRenderBody() 299 { 300 return false; 301 } 302 303 /** 304 * Used for testing. 305 */ 306 void inject(ComponentResources resources, PropertyOverrides overrides, PropertyModel propertyModel, 307 BeanBlockSource beanBlockSource, Messages messages, Object object) 308 { 309 this.resources = resources; 310 this.overrides = overrides; 311 this.propertyModel = propertyModel; 312 this.beanBlockSource = beanBlockSource; 313 this.messages = messages; 314 this.object = object; 315 } 316}