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