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.corelib.internal.InternalMessages;
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.services.BeanEditContext;
033    import org.apache.tapestry5.services.BeanModelSource;
034    import org.apache.tapestry5.services.Environment;
035    import org.apache.tapestry5.services.FormSupport;
036    
037    import java.lang.annotation.Annotation;
038    
039    /**
040     * A component that generates a user interface for editing the properties of a bean. This is the central component of
041     * the {@link BeanEditForm}, and utilizes a {@link PropertyEditor} for much of its functionality. This component places
042     * a {@link BeanEditContext} into the environment.
043     * 
044     * @tapestrydoc
045     */
046    @SupportsInformalParameters
047    public class BeanEditor
048    {
049        public static class Prepare implements ComponentAction<BeanEditor>
050        {
051            private static final long serialVersionUID = 6273600092955522585L;
052    
053            public void execute(BeanEditor component)
054            {
055                component.doPrepare();
056            }
057    
058            @Override
059            public String toString()
060            {
061                return "BeanEditor.Prepare";
062            }
063        }
064    
065        static class CleanupEnvironment implements ComponentAction<BeanEditor>
066        {
067            private static final long serialVersionUID = 6867226962459227016L;
068    
069            public void execute(BeanEditor component)
070            {
071                component.cleanupEnvironment();
072            }
073    
074            @Override
075            public String toString()
076            {
077                return "BeanEditor.CleanupEnvironment";
078            }
079        }
080    
081        private static final ComponentAction<BeanEditor> CLEANUP_ENVIRONMENT = new CleanupEnvironment();
082    
083        /**
084         * The object to be edited by the BeanEditor. This will be read when the component renders and updated when the form
085         * for the component is submitted. Typically, the container will listen for a "prepare" event, in order to ensure
086         * that a non-null value is ready to be read or updated.
087         */
088        @Parameter(autoconnect = true)
089        private Object object;
090    
091        /**
092         * A comma-separated list of property names to be retained from the
093         * {@link org.apache.tapestry5.beaneditor.BeanModel} (only used
094         * when a default model is created automatically).
095         * Only these properties will be retained, and the properties will also be reordered. The names are
096         * case-insensitive.
097         */
098        @Parameter(defaultPrefix = BindingConstants.LITERAL)
099        private String include;
100    
101        /**
102         * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}
103         * (only used
104         * when a default model is created automatically).
105         * The names are case-insensitive.
106         */
107        @Parameter(defaultPrefix = BindingConstants.LITERAL)
108        private String exclude;
109    
110        /**
111         * A comma-separated list of property names indicating the order in which the properties should be presented. The
112         * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
113         * orde. Only used
114         * when a default model is created automatically.
115         */
116        @Parameter(defaultPrefix = BindingConstants.LITERAL)
117        private String reorder;
118    
119        /**
120         * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}
121         * (only used
122         * when a default model is created automatically).
123         */
124        @Parameter(defaultPrefix = BindingConstants.LITERAL)
125        private String add;
126    
127        /**
128         * The model that identifies the parameters to be edited, their order, and every other aspect. If not specified, a
129         * default bean model will be created from the type of the object bound to the object parameter. The add, include,
130         * exclude and reorder
131         * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
132         */
133        @Parameter
134        @Property(write = false)
135        private BeanModel model;
136    
137        /**
138         * Where to search for local overrides of property editing blocks as block parameters. Further, the container of the
139         * overrides is used as the source for overridden validation messages. This is normally the BeanEditor component
140         * itself, but when the component is used within a BeanEditForm, it will be the BeanEditForm's resources that will
141         * be searched.
142         */
143        @Parameter(value = "this", allowNull = false)
144        @Property(write = false)
145        private PropertyOverrides overrides;
146    
147        @Inject
148        private BeanModelSource modelSource;
149    
150        @Inject
151        private ComponentResources resources;
152    
153        @Inject
154        private Environment environment;
155    
156        @Environmental
157        private FormSupport formSupport;
158    
159        // Value that change with each change to the current property:
160    
161        @Property
162        private String propertyName;
163    
164        /**
165         * To support nested BeanEditors, we need to cache the object value inside {@link #doPrepare()}. See TAPESTRY-2460.
166         */
167        private Object cachedObject;
168    
169        // Needed for testing as well
170    
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 = InternalMessages.failureInstantiatingObject(model.getBeanType(),
213                            resources.getCompleteId(), ex);
214                    throw new TapestryException(message, resources.getLocation(), ex);
215                }
216    
217                // If 'object' parameter is bound to a null-value BeanValidationContext is empty.
218                // This prevents JSR-303 javascript validators to be rendered properly .
219                refreshBeanValidationContext();
220            }
221    
222            BeanEditContext context = new BeanEditContext()
223            {
224                public Class<?> getBeanClass()
225                {
226                    return model.getBeanType();
227                }
228    
229                public <T extends Annotation> T getAnnotation(Class<T> type)
230                {
231                    return getBeanClass().getAnnotation(type);
232                }
233            };
234    
235            cachedObject = object;
236    
237            environment.push(BeanEditContext.class, context);
238        }
239    
240        void cleanupEnvironment()
241        {
242            environment.pop(BeanEditContext.class);
243        }
244    
245        private void refreshBeanValidationContext()
246        {
247            if (environment.peek(BeanValidationContext.class) != null)
248            {
249                environment.pop(BeanValidationContext.class);
250    
251                environment.push(BeanValidationContext.class, new BeanValidationContextImpl(object));
252            }
253        }
254    
255        // For testing
256        void inject(ComponentResources resources, PropertyOverrides overrides, BeanModelSource source,
257                Environment environment)
258        {
259            this.resources = resources;
260            this.overrides = overrides;
261            this.environment = environment;
262            modelSource = source;
263        }
264    }