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