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    }