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