001 // Copyright 2007, 2008, 2010, 2011 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 015 package org.apache.tapestry5.corelib.components; 016 017 import org.apache.tapestry5.BindingConstants; 018 import org.apache.tapestry5.ComponentAction; 019 import org.apache.tapestry5.ComponentResources; 020 import org.apache.tapestry5.PropertyOverrides; 021 import org.apache.tapestry5.annotations.Environmental; 022 import org.apache.tapestry5.annotations.Parameter; 023 import org.apache.tapestry5.annotations.Property; 024 import org.apache.tapestry5.annotations.SupportsInformalParameters; 025 import org.apache.tapestry5.beaneditor.BeanModel; 026 import org.apache.tapestry5.corelib.internal.InternalMessages; 027 import org.apache.tapestry5.internal.BeanValidationContext; 028 import org.apache.tapestry5.internal.BeanValidationContextImpl; 029 import org.apache.tapestry5.internal.beaneditor.BeanModelUtils; 030 import org.apache.tapestry5.ioc.annotations.Inject; 031 import org.apache.tapestry5.ioc.internal.util.TapestryException; 032 import org.apache.tapestry5.services.BeanEditContext; 033 import org.apache.tapestry5.services.BeanModelSource; 034 import org.apache.tapestry5.services.Environment; 035 import org.apache.tapestry5.services.FormSupport; 036 037 import java.lang.annotation.Annotation; 038 039 /** 040 * A component that generates a user interface for editing the properties of a bean. This is the central component of 041 * the {@link BeanEditForm}, and utilizes a {@link PropertyEditor} for much of its functionality. This component places 042 * a {@link BeanEditContext} into the environment. 043 * 044 * @tapestrydoc 045 */ 046 @SupportsInformalParameters 047 public class BeanEditor 048 { 049 public static class Prepare implements ComponentAction<BeanEditor> 050 { 051 private static final long serialVersionUID = 6273600092955522585L; 052 053 public void execute(BeanEditor component) 054 { 055 component.doPrepare(); 056 } 057 058 @Override 059 public String toString() 060 { 061 return "BeanEditor.Prepare"; 062 } 063 } 064 065 static class CleanupEnvironment implements ComponentAction<BeanEditor> 066 { 067 private static final long serialVersionUID = 6867226962459227016L; 068 069 public void execute(BeanEditor component) 070 { 071 component.cleanupEnvironment(); 072 } 073 074 @Override 075 public String toString() 076 { 077 return "BeanEditor.CleanupEnvironment"; 078 } 079 } 080 081 private static final ComponentAction<BeanEditor> 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(autoconnect = true) 089 private Object object; 090 091 /** 092 * A comma-separated list of property names to be retained from the 093 * {@link org.apache.tapestry5.beaneditor.BeanModel} (only used 094 * when a default model is created automatically). 095 * Only these properties will be retained, and the properties will also be reordered. The names are 096 * case-insensitive. 097 */ 098 @Parameter(defaultPrefix = BindingConstants.LITERAL) 099 private String include; 100 101 /** 102 * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel} 103 * (only used 104 * when a default model is created automatically). 105 * The names are case-insensitive. 106 */ 107 @Parameter(defaultPrefix = BindingConstants.LITERAL) 108 private String exclude; 109 110 /** 111 * A comma-separated list of property names indicating the order in which the properties should be presented. The 112 * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display 113 * orde. Only used 114 * when a default model is created automatically. 115 */ 116 @Parameter(defaultPrefix = BindingConstants.LITERAL) 117 private String reorder; 118 119 /** 120 * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel} 121 * (only used 122 * when a default model is created automatically). 123 */ 124 @Parameter(defaultPrefix = BindingConstants.LITERAL) 125 private String add; 126 127 /** 128 * The model that identifies the parameters to be edited, their order, and every other aspect. If not specified, a 129 * default bean model will be created from the type of the object bound to the object parameter. The add, include, 130 * exclude and reorder 131 * parameters are <em>only</em> applied to a default model, not an explicitly provided one. 132 */ 133 @Parameter 134 @Property(write = false) 135 private BeanModel model; 136 137 /** 138 * Where to search for local overrides of property editing blocks as block parameters. Further, the container of the 139 * overrides is used as the source for overridden validation messages. This is normally the BeanEditor component 140 * itself, but when the component is used within a BeanEditForm, it will be the BeanEditForm's resources that will 141 * be searched. 142 */ 143 @Parameter(value = "this", allowNull = false) 144 @Property(write = false) 145 private PropertyOverrides overrides; 146 147 @Inject 148 private BeanModelSource modelSource; 149 150 @Inject 151 private ComponentResources resources; 152 153 @Inject 154 private Environment environment; 155 156 @Environmental 157 private FormSupport formSupport; 158 159 // Value that change with each change to the current property: 160 161 @Property 162 private String propertyName; 163 164 /** 165 * To support nested BeanEditors, we need to cache the object value inside {@link #doPrepare()}. See TAPESTRY-2460. 166 */ 167 private Object cachedObject; 168 169 // Needed for testing as well 170 171 public Object getObject() 172 { 173 return cachedObject; 174 } 175 176 void setupRender() 177 { 178 formSupport.storeAndExecute(this, new Prepare()); 179 } 180 181 void cleanupRender() 182 { 183 formSupport.storeAndExecute(this, CLEANUP_ENVIRONMENT); 184 } 185 186 /** 187 * Used to initialize the model if necessary, to instantiate the object being edited if necessary, and to push the 188 * BeanEditContext into the environment. 189 */ 190 void doPrepare() 191 { 192 if (model == null) 193 { 194 Class type = resources.getBoundType("object"); 195 model = modelSource.createEditModel(type, overrides.getOverrideMessages()); 196 197 BeanModelUtils.modify(model, add, include, exclude, reorder); 198 } 199 200 // The only problem here is that if the bound property is backed by a persistent field, it 201 // is assigned (and stored to the session, and propagated around the cluster) first, 202 // before values are assigned. 203 204 if (object == null) 205 { 206 try 207 { 208 object = model.newInstance(); 209 } 210 catch (Exception ex) 211 { 212 String message = InternalMessages.failureInstantiatingObject(model.getBeanType(), 213 resources.getCompleteId(), ex); 214 throw new TapestryException(message, resources.getLocation(), ex); 215 } 216 217 // If 'object' parameter is bound to a null-value BeanValidationContext is empty. 218 // This prevents JSR-303 javascript validators to be rendered properly . 219 refreshBeanValidationContext(); 220 } 221 222 BeanEditContext context = new BeanEditContext() 223 { 224 public Class<?> getBeanClass() 225 { 226 return model.getBeanType(); 227 } 228 229 public <T extends Annotation> T getAnnotation(Class<T> type) 230 { 231 return getBeanClass().getAnnotation(type); 232 } 233 }; 234 235 cachedObject = object; 236 237 environment.push(BeanEditContext.class, context); 238 } 239 240 void cleanupEnvironment() 241 { 242 environment.pop(BeanEditContext.class); 243 } 244 245 private void refreshBeanValidationContext() 246 { 247 if (environment.peek(BeanValidationContext.class) != null) 248 { 249 environment.pop(BeanValidationContext.class); 250 251 environment.push(BeanValidationContext.class, new BeanValidationContextImpl(object)); 252 } 253 } 254 255 // For testing 256 void inject(ComponentResources resources, PropertyOverrides overrides, BeanModelSource source, 257 Environment environment) 258 { 259 this.resources = resources; 260 this.overrides = overrides; 261 this.environment = environment; 262 modelSource = source; 263 } 264 }