001// Copyright 2007-2013 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    @Inject
144    private Heartbeat heartbeat;
145
146    private PropertyModel propertyModel;
147
148    BeanBlockSource defaultBeanBlockSource()
149    {
150        return defaultBeanBlockSource;
151    }
152
153    /**
154     * Creates a {@link org.apache.tapestry5.services.PropertyEditContext} and pushes it onto the {@link
155     * org.apache.tapestry5.services.Environment} stack.
156     */
157    void setupEnvironment(final String propertyName)
158    {
159        propertyModel = model.get(propertyName);
160
161        PropertyEditContext context = new PropertyEditContext()
162        {
163            public Messages getContainerMessages()
164            {
165                return overrides.getOverrideMessages();
166            }
167
168            public String getLabel()
169            {
170                return propertyModel.getLabel();
171            }
172
173            public String getPropertyId()
174            {
175                return propertyModel.getId();
176            }
177
178            public Class getPropertyType()
179            {
180                return propertyModel.getPropertyType();
181            }
182
183            public Object getPropertyValue()
184            {
185                return propertyModel.getConduit().get(object);
186            }
187
188            public FieldTranslator getTranslator(Field field)
189            {
190                return fieldTranslatorSource.createDefaultTranslator(field, propertyName,
191                                                                     overrides.getOverrideMessages(), locale,
192                                                                     propertyModel.getPropertyType(),
193                                                                     propertyModel.getConduit());
194            }
195
196            public FieldValidator getValidator(Field field)
197            {
198                return fieldValidatorDefaultSource.createDefaultValidator(field, propertyName,
199                                                                          overrides.getOverrideMessages(), locale,
200                                                                          propertyModel.getPropertyType(),
201                                                                          propertyModel.getConduit());
202            }
203
204            public void setPropertyValue(Object value)
205            {
206                propertyModel.getConduit().set(object, value);
207            }
208
209            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
210            {
211                return propertyModel.getAnnotation(annotationClass);
212            }
213        };
214
215        environment.push(PropertyEditContext.class, context);
216        
217        BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
218        
219        if(beanValidationContext != null)
220        {
221            beanValidationContext.setCurrentProperty(propertyName);
222        }
223
224        heartbeat.begin();
225    }
226
227    /**
228     * Called at the end of the form render (or at the end of the form submission) to clean up the {@link Environment}
229     * stack.
230     */
231    void cleanupEnvironment()
232    {
233        heartbeat.end();
234
235        environment.pop(PropertyEditContext.class);
236    }
237
238    /**
239     * Record into the Form what's needed to process the property.
240     */
241    void setupRender()
242    {
243        // Sets up the PropertyEditContext for the duration of the render of this component
244        // (which will include the duration of the editor block).
245
246        formSupport.storeAndExecute(this, new SetupEnvironment(property));
247    }
248
249    /**
250     * Records into the Form the cleanup logic for the property.
251     */
252    void cleanupRender()
253    {
254        // Removes the PropertyEditContext after this component (including the editor block)
255        // has rendered.
256
257        formSupport.storeAndExecute(this, CLEANUP_ENVIRONMENT);
258    }
259
260    /**
261     * Returns a Block for rendering the property. The Block will be able to access the {@link PropertyEditContext} via
262     * the {@link Environmental} annotation.
263     */
264    Block beginRender()
265    {
266        
267        Block override = overrides.getOverrideBlock(propertyModel.getId());
268
269        if (override != null)
270        {
271            return override;
272        }
273
274        String dataType = propertyModel.getDataType();
275
276        if (dataType == null)
277            throw new RuntimeException(
278                    String.format("The data type for property '%s' of %s is null.", propertyModel.getPropertyName(),
279                                  object));
280
281        try
282        {
283
284            return beanBlockSource.getEditBlock(dataType);
285        }
286        catch (RuntimeException ex)
287        {
288            String message = messages.format("core-block-error", propertyModel.getPropertyName(), dataType, object, ex);
289
290            throw new TapestryException(message, resources.getLocation(), ex);
291        }
292        
293    }
294
295    /**
296     * Returns false, to prevent the rendering of the body of the component. PropertyEditor should not have a body.
297     */
298    boolean beforeRenderBody()
299    {
300        return false;
301    }
302
303    /**
304     * Used for testing.
305     */
306    void inject(ComponentResources resources, PropertyOverrides overrides, PropertyModel propertyModel,
307                BeanBlockSource beanBlockSource, Messages messages, Object object)
308    {
309        this.resources = resources;
310        this.overrides = overrides;
311        this.propertyModel = propertyModel;
312        this.beanBlockSource = beanBlockSource;
313        this.messages = messages;
314        this.object = object;
315    }
316}