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