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
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.BindingConstants;
018import org.apache.tapestry5.ComponentAction;
019import org.apache.tapestry5.ComponentResources;
020import org.apache.tapestry5.PropertyOverrides;
021import org.apache.tapestry5.annotations.Environmental;
022import org.apache.tapestry5.annotations.Parameter;
023import org.apache.tapestry5.annotations.Property;
024import org.apache.tapestry5.annotations.SupportsInformalParameters;
025import org.apache.tapestry5.beaneditor.BeanModel;
026import org.apache.tapestry5.internal.BeanEditContextImpl;
027import org.apache.tapestry5.internal.BeanValidationContext;
028import org.apache.tapestry5.internal.BeanValidationContextImpl;
029import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
030import org.apache.tapestry5.ioc.annotations.Inject;
031import org.apache.tapestry5.ioc.internal.util.TapestryException;
032import org.apache.tapestry5.plastic.PlasticUtils;
033import org.apache.tapestry5.services.BeanEditContext;
034import org.apache.tapestry5.services.BeanModelSource;
035import org.apache.tapestry5.services.Environment;
036import org.apache.tapestry5.services.FormSupport;
037
038import 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
048public 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}