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.internal.BeanEditContextImpl;
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.plastic.PlasticUtils;
033 import org.apache.tapestry5.services.BeanEditContext;
034 import org.apache.tapestry5.services.BeanModelSource;
035 import org.apache.tapestry5.services.Environment;
036 import org.apache.tapestry5.services.FormSupport;
037
038 import java.lang.annotation.Annotation;
039
040 /**
041 * A component that generates a user interface for editing the properties of a bean. This is the central component of
042 * the {@link BeanEditForm}, and utilizes a {@link PropertyEditor} for much of its functionality. This component places
043 * a {@link BeanEditContext} into the environment.
044 *
045 * @tapestrydoc
046 */
047 @SupportsInformalParameters
048 public class BeanEditor
049 {
050 public static class Prepare implements ComponentAction<BeanEditor>
051 {
052 private static final long serialVersionUID = 6273600092955522585L;
053
054 public void execute(BeanEditor component)
055 {
056 component.doPrepare();
057 }
058
059 @Override
060 public String toString()
061 {
062 return "BeanEditor.Prepare";
063 }
064 }
065
066 static class CleanupEnvironment implements ComponentAction<BeanEditor>
067 {
068 private static final long serialVersionUID = 6867226962459227016L;
069
070 public void execute(BeanEditor component)
071 {
072 component.cleanupEnvironment();
073 }
074
075 @Override
076 public String toString()
077 {
078 return "BeanEditor.CleanupEnvironment";
079 }
080 }
081
082 private static final ComponentAction<BeanEditor> CLEANUP_ENVIRONMENT = new CleanupEnvironment();
083
084 /**
085 * The object to be edited by the BeanEditor. This will be read when the component renders and updated when the form
086 * for the component is submitted. Typically, the container will listen for a "prepare" event, in order to ensure
087 * that a non-null value is ready to be read or updated.
088 */
089 @Parameter(autoconnect = true)
090 private Object object;
091
092 /**
093 * A comma-separated list of property names to be retained from the
094 * {@link org.apache.tapestry5.beaneditor.BeanModel} (only used
095 * when a default model is created automatically).
096 * Only these properties will be retained, and the properties will also be reordered. The names are
097 * case-insensitive.
098 */
099 @Parameter(defaultPrefix = BindingConstants.LITERAL)
100 private String include;
101
102 /**
103 * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}
104 * (only used
105 * when a default model is created automatically).
106 * The names are case-insensitive.
107 */
108 @Parameter(defaultPrefix = BindingConstants.LITERAL)
109 private String exclude;
110
111 /**
112 * A comma-separated list of property names indicating the order in which the properties should be presented. The
113 * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
114 * orde. Only used
115 * when a default model is created automatically.
116 */
117 @Parameter(defaultPrefix = BindingConstants.LITERAL)
118 private String reorder;
119
120 /**
121 * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}
122 * (only used
123 * when a default model is created automatically).
124 */
125 @Parameter(defaultPrefix = BindingConstants.LITERAL)
126 private String add;
127
128 /**
129 * The model that identifies the parameters to be edited, their order, and every other aspect. If not specified, a
130 * default bean model will be created from the type of the object bound to the object parameter. The add, include,
131 * exclude and reorder
132 * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
133 */
134 @Parameter
135 @Property(write = false)
136 private BeanModel model;
137
138 /**
139 * Where to search for local overrides of property editing blocks as block parameters. Further, the container of the
140 * overrides is used as the source for overridden validation messages. This is normally the BeanEditor component
141 * itself, but when the component is used within a BeanEditForm, it will be the BeanEditForm's resources that will
142 * be searched.
143 */
144 @Parameter(value = "this", allowNull = false)
145 @Property(write = false)
146 private PropertyOverrides overrides;
147
148 @Inject
149 private BeanModelSource modelSource;
150
151 @Inject
152 private ComponentResources resources;
153
154 @Inject
155 private Environment environment;
156
157 @Environmental
158 private FormSupport formSupport;
159
160 // Value that change with each change to the current property:
161
162 @Property
163 private String propertyName;
164
165 /**
166 * To support nested BeanEditors, we need to cache the object value inside {@link #doPrepare()}. See TAPESTRY-2460.
167 */
168 private Object cachedObject;
169
170 // Needed for testing as well
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 = String.format("Exception instantiating instance of %s (for component '%s'): %s",
213 PlasticUtils.toTypeName(model.getBeanType()), resources.getCompleteId(), ex);
214 throw new TapestryException(message, resources.getLocation(), ex);
215 }
216 }
217
218 BeanEditContext context = new BeanEditContextImpl(model.getBeanType());
219
220 cachedObject = object;
221
222 environment.push(BeanEditContext.class, context);
223 // TAP5-2101: Always provide a new BeanValidationContext
224 environment.push(BeanValidationContext.class, new BeanValidationContextImpl(object));
225 }
226
227 void cleanupEnvironment()
228 {
229 environment.pop(BeanEditContext.class);
230 environment.pop(BeanValidationContext.class);
231 }
232
233 // For testing
234 void inject(ComponentResources resources, PropertyOverrides overrides, BeanModelSource source,
235 Environment environment)
236 {
237 this.resources = resources;
238 this.overrides = overrides;
239 this.environment = environment;
240 modelSource = source;
241 }
242 }