    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}