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