    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
    015package org.apache.tapestry5.corelib.components;
    016
    017import org.apache.tapestry5.BindingConstants;
    018import org.apache.tapestry5.ComponentAction;
    019import org.apache.tapestry5.ComponentResources;
    020import org.apache.tapestry5.PropertyOverrides;
    021import org.apache.tapestry5.annotations.Environmental;
    022import org.apache.tapestry5.annotations.Parameter;
    023import org.apache.tapestry5.annotations.Property;
    024import org.apache.tapestry5.annotations.SupportsInformalParameters;
    025import org.apache.tapestry5.beaneditor.BeanModel;
    026import org.apache.tapestry5.internal.BeanEditContextImpl;
    027import org.apache.tapestry5.internal.BeanValidationContext;
    028import org.apache.tapestry5.internal.BeanValidationContextImpl;
    029import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
    030import org.apache.tapestry5.ioc.annotations.Inject;
    031import org.apache.tapestry5.ioc.internal.util.TapestryException;
    032import org.apache.tapestry5.plastic.PlasticUtils;
    033import org.apache.tapestry5.services.BeanEditContext;
    034import org.apache.tapestry5.services.BeanModelSource;
    035import org.apache.tapestry5.services.Environment;
    036import org.apache.tapestry5.services.FormSupport;
    037
    038import 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
    048public 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}