001// Copyright 2007, 2008, 2009, 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.*;
018import org.apache.tapestry5.annotations.Environmental;
019import org.apache.tapestry5.annotations.Parameter;
020import org.apache.tapestry5.beaneditor.BeanModel;
021import org.apache.tapestry5.beaneditor.PropertyModel;
022import org.apache.tapestry5.internal.BeanValidationContext;
023import org.apache.tapestry5.ioc.Messages;
024import org.apache.tapestry5.ioc.annotations.Inject;
025import org.apache.tapestry5.ioc.internal.util.TapestryException;
026import org.apache.tapestry5.services.*;
027
028import java.lang.annotation.Annotation;
029import java.util.Locale;
030
031/**
032 * Used to edit a single property of a bean. This is used primarily by {@link BeanEditForm}. Unlike BeanEditForm, the
033 * object to be edited must already exist and the {@linkplain BeanModel model} must be passed in explicitly.
034 * 
035 * @tapestrydoc
036 */
037public class PropertyEditor
038{
039    /**
040     * Configures and stores a {@link PropertyEditContext} into the {@link Environment}.
041     */
042    static class SetupEnvironment implements ComponentAction<PropertyEditor>
043    {
044        private static final long serialVersionUID = 5337049721509981997L;
045
046        private final String property;
047
048        public SetupEnvironment(String property)
049        {
050            this.property = property;
051        }
052
053        public void execute(PropertyEditor component)
054        {
055            component.setupEnvironment(property);
056        }
057
058        @Override
059        public String toString()
060        {
061            return String.format("PropertyEditor.SetupEnvironment[%s]", property);
062        }
063    }
064
065    static class CleanupEnvironment implements ComponentAction<PropertyEditor>
066    {
067        private static final long serialVersionUID = 7878694042753046523L;
068
069        public void execute(PropertyEditor component)
070        {
071            component.cleanupEnvironment();
072        }
073
074        @Override
075        public String toString()
076        {
077            return "PropertyEditor.CleanupEnvironment";
078        }
079    }
080
081    private static final ComponentAction 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(required = true, allowNull = false)
089    private Object object;
090
091    /**
092     * Where to search for local overrides of property editing blocks as block parameters. This is normally the
093     * containing component of the PropertyEditor, but when the component is used within a BeanEditor, it will be the
094     * BeanEditor's block parameters that will be searched.
095     */
096    @Parameter(value = "this", allowNull = false)
097    private PropertyOverrides overrides;
098
099    /**
100     * Identifies the property to be edited by the editor.
101     */
102    @Parameter(required = true)
103    private String property;
104
105    /**
106     * The model that identifies the parameters to be edited, their order, and every other aspect.
107     */
108    @Parameter(required = true, allowNull = false)
109    private BeanModel model;
110
111    @Inject
112    private FieldValidatorDefaultSource fieldValidatorDefaultSource;
113
114    @Inject
115    private Environment environment;
116
117    /**
118     * Source for property editor blocks. This defaults to the default implementation of {@link
119     * org.apache.tapestry5.services.BeanBlockSource}.
120     */
121    @Parameter(required = true, allowNull = false)
122    private BeanBlockSource beanBlockSource;
123
124    @Inject
125    @Core
126    private BeanBlockSource defaultBeanBlockSource;
127
128    @Inject
129    private Messages messages;
130
131    @Inject
132    private Locale locale;
133
134    @Inject
135    private ComponentResources resources;
136
137    @Inject
138    private FieldTranslatorSource fieldTranslatorSource;
139
140    @Environmental
141    private FormSupport formSupport;
142
143    private PropertyModel propertyModel;
144
145    BeanBlockSource defaultBeanBlockSource()
146    {
147        return defaultBeanBlockSource;
148    }
149
150    /**
151     * Creates a {@link org.apache.tapestry5.services.PropertyEditContext} and pushes it onto the {@link
152     * org.apache.tapestry5.services.Environment} stack.
153     */
154    void setupEnvironment(final String propertyName)
155    {
156        propertyModel = model.get(propertyName);
157
158        PropertyEditContext context = new PropertyEditContext()
159        {
160            public Messages getContainerMessages()
161            {
162                return overrides.getOverrideMessages();
163            }
164
165            public String getLabel()
166            {
167                return propertyModel.getLabel();
168            }
169
170            public String getPropertyId()
171            {
172                return propertyModel.getId();
173            }
174
175            public Class getPropertyType()
176            {
177                return propertyModel.getPropertyType();
178            }
179
180            public Object getPropertyValue()
181            {
182                return propertyModel.getConduit().get(object);
183            }
184
185            public FieldTranslator getTranslator(Field field)
186            {
187                return fieldTranslatorSource.createDefaultTranslator(field, propertyName,
188                                                                     overrides.getOverrideMessages(), locale,
189                                                                     propertyModel.getPropertyType(),
190                                                                     propertyModel.getConduit());
191            }
192
193            public FieldValidator getValidator(Field field)
194            {
195                return fieldValidatorDefaultSource.createDefaultValidator(field, propertyName,
196                                                                          overrides.getOverrideMessages(), locale,
197                                                                          propertyModel.getPropertyType(),
198                                                                          propertyModel.getConduit());
199            }
200
201            public void setPropertyValue(Object value)
202            {
203                propertyModel.getConduit().set(object, value);
204            }
205
206            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
207            {
208                return propertyModel.getAnnotation(annotationClass);
209            }
210        };
211
212        environment.push(PropertyEditContext.class, context);
213        
214        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
215        
216        if(beanValidationContext != null)
217        {
218                beanValidationContext.setCurrentProperty(propertyName);
219        }
220    }
221
222    /**
223     * Called at the end of the form render (or at the end of the form submission) to clean up the {@link Environment}
224     * stack.
225     */
226    void cleanupEnvironment()
227    {
228        environment.pop(PropertyEditContext.class);
229    }
230
231    /**
232     * Record into the Form what's needed to process the property.
233     */
234    void setupRender()
235    {
236        // Sets up the PropertyEditContext for the duration of the render of this component
237        // (which will include the duration of the editor block).
238
239        formSupport.storeAndExecute(this, new SetupEnvironment(property));
240    }
241
242    /**
243     * Records into the Form the cleanup logic for the property.
244     */
245    void cleanupRender()
246    {
247        // Removes the PropertyEditContext after this component (including the editor block)
248        // has rendered.
249
250        formSupport.storeAndExecute(this, CLEANUP_ENVIRONMENT);
251    }
252
253    /**
254     * Returns a Block for rendering the property. The Block will be able to access the {@link PropertyEditContext} via
255     * the {@link Environmental} annotation.
256     */
257    Block beginRender()
258    {
259        
260        Block override = overrides.getOverrideBlock(propertyModel.getId());
261
262        if (override != null)
263        {
264            return override;
265        }
266
267        String dataType = propertyModel.getDataType();
268
269        if (dataType == null)
270            throw new RuntimeException(
271                    String.format("The data type for property '%s' of %s is null.", propertyModel.getPropertyName(),
272                                  object));
273
274        try
275        {
276
277            return beanBlockSource.getEditBlock(dataType);
278        }
279        catch (RuntimeException ex)
280        {
281            String message = messages.format("block-error", propertyModel.getPropertyName(), dataType, object, ex);
282
283            throw new TapestryException(message, resources.getLocation(), ex);
284        }
285        
286    }
287
288    /**
289     * Returns false, to prevent the rendering of the body of the component. PropertyEditor should not have a body.
290     */
291    boolean beforeRenderBody()
292    {
293        return false;
294    }
295
296    /**
297     * Used for testing.
298     */
299    void inject(ComponentResources resources, PropertyOverrides overrides, PropertyModel propertyModel,
300                BeanBlockSource beanBlockSource, Messages messages, Object object)
301    {
302        this.resources = resources;
303        this.overrides = overrides;
304        this.propertyModel = propertyModel;
305        this.beanBlockSource = beanBlockSource;
306        this.messages = messages;
307        this.object = object;
308    }
309}