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