001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.BindingConstants; 016import org.apache.tapestry5.ComponentAction; 017import org.apache.tapestry5.ComponentResources; 018import org.apache.tapestry5.PropertyOverrides; 019import org.apache.tapestry5.annotations.Environmental; 020import org.apache.tapestry5.annotations.Parameter; 021import org.apache.tapestry5.annotations.Property; 022import org.apache.tapestry5.annotations.SupportsInformalParameters; 023import org.apache.tapestry5.beanmodel.BeanModel; 024import org.apache.tapestry5.beanmodel.BeanModelUtils; 025import org.apache.tapestry5.beanmodel.services.BeanModelSource; 026import org.apache.tapestry5.commons.internal.util.TapestryException; 027import org.apache.tapestry5.internal.BeanEditContextImpl; 028import org.apache.tapestry5.internal.BeanValidationContext; 029import org.apache.tapestry5.internal.BeanValidationContextImpl; 030import org.apache.tapestry5.ioc.annotations.Inject; 031import org.apache.tapestry5.plastic.PlasticUtils; 032import org.apache.tapestry5.services.BeanEditContext; 033import org.apache.tapestry5.services.Environment; 034import org.apache.tapestry5.services.FormSupport; 035 036/** 037 * A component that generates a user interface for editing the properties of a bean (or POJO, 038 * or any object with properties). This is the central component of the {@link BeanEditForm} 039 * component, and it utilizes the {@link PropertyEditor} component for much of its functionality. 040 * <p> 041 * This component places a {@link BeanEditContext} into the {@link Environment}. 042 * 043 * @tapestrydoc 044 */ 045@SupportsInformalParameters 046public class BeanEditor 047{ 048 public static class Prepare implements ComponentAction<BeanEditor> 049 { 050 private static final long serialVersionUID = 6273600092955522585L; 051 052 public void execute(BeanEditor component) 053 { 054 component.doPrepare(); 055 } 056 057 @Override 058 public String toString() 059 { 060 return "BeanEditor.Prepare"; 061 } 062 } 063 064 static class CleanupEnvironment implements ComponentAction<BeanEditor> 065 { 066 private static final long serialVersionUID = 6867226962459227016L; 067 068 public void execute(BeanEditor component) 069 { 070 component.cleanupEnvironment(); 071 } 072 073 @Override 074 public String toString() 075 { 076 return "BeanEditor.CleanupEnvironment"; 077 } 078 } 079 080 private static final ComponentAction<BeanEditor> CLEANUP_ENVIRONMENT = new CleanupEnvironment(); 081 082 /** 083 * The object to be edited by the BeanEditor. This will be read when the component renders and updated when the form 084 * for the component is submitted. Typically, the container will listen for a "prepare" event, in order to ensure 085 * that a non-null value is ready to be read or updated. 086 */ 087 @Parameter(autoconnect = true) 088 private Object object; 089 090 /** 091 * A comma-separated list of property names to be retained from the 092 * {@link org.apache.tapestry5.beanmodel.BeanModel} (only used 093 * when a default model is created automatically). 094 * Only these properties will be retained, and the properties will also be reordered. The names are 095 * case-insensitive. 096 */ 097 @Parameter(defaultPrefix = BindingConstants.LITERAL) 098 private String include; 099 100 /** 101 * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beanmodel.BeanModel} 102 * (only used 103 * when a default model is created automatically). 104 * The names are case-insensitive. 105 */ 106 @Parameter(defaultPrefix = BindingConstants.LITERAL) 107 private String exclude; 108 109 /** 110 * A comma-separated list of property names indicating the order in which the properties should be presented. The 111 * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display 112 * orde. Only used 113 * when a default model is created automatically. 114 */ 115 @Parameter(defaultPrefix = BindingConstants.LITERAL) 116 private String reorder; 117 118 /** 119 * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beanmodel.BeanModel} 120 * (only used 121 * when a default model is created automatically). 122 */ 123 @Parameter(defaultPrefix = BindingConstants.LITERAL) 124 private String add; 125 126 /** 127 * The model that identifies the parameters to be edited, their order, and every other aspect. If not specified, a 128 * default bean model will be created from the type of the object bound to the object parameter. The add, include, 129 * exclude and reorder 130 * parameters are <em>only</em> applied to a default model, not an explicitly provided one. 131 */ 132 @Parameter 133 @Property(write = false) 134 private BeanModel model; 135 136 /** 137 * Where to search for local overrides of property editing blocks as block parameters. Further, the container of the 138 * overrides is used as the source for overridden validation messages. This is normally the BeanEditor component 139 * itself, but when the component is used within a BeanEditForm, it will be the BeanEditForm's resources that will 140 * be searched. 141 */ 142 @Parameter(value = "this", allowNull = false) 143 @Property(write = false) 144 private PropertyOverrides overrides; 145 146 @Inject 147 private BeanModelSource modelSource; 148 149 @Inject 150 private ComponentResources resources; 151 152 @Inject 153 private Environment environment; 154 155 @Environmental 156 private FormSupport formSupport; 157 158 // Value that change with each change to the current property: 159 160 @Property 161 private String propertyName; 162 163 /** 164 * To support nested BeanEditors, we need to cache the object value inside {@link #doPrepare()}. See TAPESTRY-2460. 165 */ 166 private Object cachedObject; 167 168 // Needed for testing as well 169 public Object getObject() 170 { 171 return cachedObject; 172 } 173 174 void setupRender() 175 { 176 formSupport.storeAndExecute(this, new Prepare()); 177 } 178 179 void cleanupRender() 180 { 181 formSupport.storeAndExecute(this, CLEANUP_ENVIRONMENT); 182 } 183 184 /** 185 * Used to initialize the model if necessary, to instantiate the object being edited if necessary, and to push the 186 * BeanEditContext into the environment. 187 */ 188 void doPrepare() 189 { 190 if (model == null) 191 { 192 Class type = resources.getBoundType("object"); 193 model = modelSource.createEditModel(type, overrides.getOverrideMessages()); 194 195 BeanModelUtils.modify(model, add, include, exclude, reorder); 196 } 197 198 // The only problem here is that if the bound property is backed by a persistent field, it 199 // is assigned (and stored to the session, and propagated around the cluster) first, 200 // before values are assigned. 201 202 if (object == null) 203 { 204 try 205 { 206 object = model.newInstance(); 207 } 208 catch (Exception ex) 209 { 210 String message = String.format("Exception instantiating instance of %s (for component '%s'): %s", 211 PlasticUtils.toTypeName(model.getBeanType()), resources.getCompleteId(), ex); 212 throw new TapestryException(message, resources.getLocation(), ex); 213 } 214 } 215 216 BeanEditContext context = new BeanEditContextImpl(model.getBeanType()); 217 218 cachedObject = object; 219 220 environment.push(BeanEditContext.class, context); 221 // TAP5-2101: Always provide a new BeanValidationContext 222 environment.push(BeanValidationContext.class, new BeanValidationContextImpl(object)); 223 } 224 225 void cleanupEnvironment() 226 { 227 environment.pop(BeanEditContext.class); 228 environment.pop(BeanValidationContext.class); 229 } 230 231 // For testing 232 void inject(ComponentResources resources, PropertyOverrides overrides, BeanModelSource source, 233 Environment environment) 234 { 235 this.resources = resources; 236 this.overrides = overrides; 237 this.environment = environment; 238 modelSource = source; 239 } 240}