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