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 (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.beaneditor.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.beaneditor.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.beaneditor.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}