    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.*;
    016import org.apache.tapestry5.annotations.*;
    017import org.apache.tapestry5.beanmodel.BeanModel;
    018import org.apache.tapestry5.beanmodel.BeanModelUtils;
    019import org.apache.tapestry5.beanmodel.PropertyModel;
    020import org.apache.tapestry5.beanmodel.services.BeanModelSource;
    021import org.apache.tapestry5.corelib.data.GridPagerPosition;
    022import org.apache.tapestry5.grid.*;
    023import org.apache.tapestry5.internal.TapestryInternalUtils;
    024import org.apache.tapestry5.internal.bindings.AbstractBinding;
    025import org.apache.tapestry5.ioc.annotations.Inject;
    026import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    027import org.apache.tapestry5.services.ComponentDefaultProvider;
    028import org.apache.tapestry5.services.ComponentEventResultProcessor;
    029import org.apache.tapestry5.services.FormSupport;
    030import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    031
    032import java.io.IOException;
    033import java.util.Collections;
    034import java.util.List;
    035
    036/**
    037 * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The
    038 * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need.
    039 *
    040 * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of
    041 * properties, the default output-only behavior can be changed to produce a complex form with individual control for
    042 * editing properties of each row. There is a big caveat here: if the order of rows provided by
    043 * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the
    044 * possibility that data will be applied to the wrong server-side objects.
    045 *
    046 * For this reason, when using Grid and Form together, you should generally
    047 * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the
    048 * encoder parameter), or use an entity type for the "row" parameter for which
    049 * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
    050 * to use a unique ID for each row that doesn't change when rows are reordered.
    051 *
    052 * @tapestrydoc
    053 * @see org.apache.tapestry5.beanmodel.BeanModel
    054 * @see org.apache.tapestry5.beanmodel.services.BeanModelSource
    055 * @see org.apache.tapestry5.grid.GridDataSource
    056 * @see BeanEditForm
    057 * @see BeanDisplay
    058 * @see Loop
    059 */
    060@SupportsInformalParameters
    061public class Grid implements GridModel, ClientElement
    062{
    063    /**
    064     * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit
    065     * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper
    066     * around the underlying List.
    067     */
    068    @Parameter(required = true, autoconnect = true)
    069    private GridDataSource source;
    070
    071    /**
    072     * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source
    073     * provided to sub-components.
    074     */
    075    private GridDataSource cachingSource;
    076
    077    /**
    078     * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up
    079     * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result
    080     * set.
    081     */
    082    @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE)
    083    private int rowsPerPage;
    084
    085    /**
    086     * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom",
    087     * "both" or "none".
    088     */
    089    @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION,
    090            defaultPrefix = BindingConstants.LITERAL)
    091    private GridPagerPosition pagerPosition;
    092
    093    /**
    094     * Used to store the current object being rendered (for the current row). This is used when parameter blocks are
    095     * provided to override the default cell renderer for a particular column ... the components within the block can
    096     * use the property bound to the row parameter to know what they should render.
    097     */
    098    @Parameter(principal = true)
    099    private Object row;
    100
    101    /**
    102     * Optional output parameter used to identify the index of the column being rendered.
    103     */
    104    @Parameter
    105    private int columnIndex;
    106
    107    /**
    108     * The model used to identify the properties to be presented and the order of presentation. The model may be
    109     * omitted, in which case a default model is generated from the first object in the data source (this implies that
    110     * the objects provided by the source are uniform). The model may be explicitly specified to override the default
    111     * behavior, say to reorder or rename columns or add additional columns. The add, include,
    112     * exclude and reorder
    113     * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
    114     */
    115    @Parameter
    116    private BeanModel model;
    117
    118    /**
    119     * The model parameter after modification due to the add, include, exclude and reorder parameters.
    120     */
    121    private BeanModel dataModel;
    122
    123    /**
    124     * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports
    125     * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is
    126     * stored as persistent fields of the Grid component.
    127     */
    128    @Parameter
    129    private GridSortModel sortModel;
    130
    131    /**
    132     * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beanmodel.BeanModel}.
    133     * Cells for added columns will be blank unless a cell override is provided. This parameter is only used
    134     * when a default model is created automatically.
    135     */
    136    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    137    private String add;
    138
    139    /**
    140     * A comma-separated list of property names to be retained from the
    141     * {@link org.apache.tapestry5.beanmodel.BeanModel}.
    142     * Only these properties will be retained, and the properties will also be reordered. The names are
    143     * case-insensitive. This parameter is only used
    144     * when a default model is created automatically.
    145     */
    146    @SuppressWarnings("unused")
    147    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    148    private String include;
    149
    150    /**
    151     * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beanmodel.BeanModel}
    152     * .
    153     * The names are case-insensitive. This parameter is only used
    154     * when a default model is created automatically.
    155     */
    156    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    157    private String exclude;
    158
    159    /**
    160     * A comma-separated list of property names indicating the order in which the properties should be presented. The
    161     * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
    162     * order. This parameter is only used
    163     * when a default model is created automatically.
    164     */
    165    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    166    private String reorder;
    167
    168    /**
    169     * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text
    170     * "There is no data to display". This parameter is used to customize that message, possibly including components to
    171     * allow the user to create new objects.
    172     */
    173    //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK,
    174    @Parameter(value = "block:empty",
    175            defaultPrefix = BindingConstants.LITERAL)
    176    @Property(write = false)
    177    private Block empty;
    178
    179    /**
    180     * CSS class for the &lt;table&gt; element. In addition, informal parameters to the Grid are rendered in the table
    181     * element.
    182     */
    183    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,
    184            value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS)
    185    @Property(write = false)
    186    private String tableClass;
    187
    188    /**
    189     * If true, then the Grid will be wrapped in an element that acts like a
    190     * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone,
    191     * repainting the entire grid within it, but leaving the rest of the page (outside the zone) unchanged.
    192     */
    193    @Parameter
    194    private boolean inPlace;
    195
    196    /**
    197     * If true, then the Grid will also render a table element complete with headers if the data source is empty.
    198     * If set to true, a model parameter will have to be specified. A default model for a specific class can be
    199     * created using {@link BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)}.
    200     */
    201    @Parameter
    202    private boolean renderTableIfEmpty = false;
    203
    204
    205    /**
    206     * The name of the pseudo-zone that encloses the Grid. Starting in 5.4, this is always either
    207     * null or "^" and is not really used the way it was in 5.3; instead it triggers the addition
    208     * of a {@code data-inplace-grid-links} attribute in a div surrounding any links related to
    209     * sorting or pagination. The rest is sorted out on the client. See module {@code t5/core/zone}.
    210     */
    211    @Property(write = false)
    212    private String zone;
    213
    214    private boolean didRenderZoneDiv;
    215
    216
    217    /**
    218     * The pagination model for the Grid, which encapsulates current page, sort column id,
    219     * and sort ascending/descending. If not bound, a persistent property of the Grid is used.
    220     * When rendering the Grid in a loop, this should be bound in some way to keep successive instances
    221     * of the Grid configured individually.
    222     *
    223     * @since 5.4
    224     */
    225    @Parameter(value = "defaultPaginationModel")
    226    private GridPaginationModel paginationModel;
    227
    228    @Persist
    229    private GridPaginationModel defaultPaginationModel;
    230
    231    @Inject
    232    private ComponentResources resources;
    233
    234    @Inject
    235    private BeanModelSource modelSource;
    236
    237    @Environmental
    238    private JavaScriptSupport javaScriptSupport;
    239
    240    @Component(parameters =
    241            {"index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone"})
    242    private GridColumns columns;
    243
    244    @Component(parameters =
    245            {"columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row",
    246                    "overrides=overrides"}, publishParameters = "rowIndex,rowClass,volatile,encoder,lean")
    247    private GridRows rows;
    248
    249    @Component(parameters =
    250            {"source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone"})
    251    private GridPager pager;
    252
    253    @Component(parameters = "to=pagerTop")
    254    private Delegate pagerTop;
    255
    256    @Component(parameters = "to=pagerBottom")
    257    private Delegate pagerBottom;
    258
    259    @Component(parameters = "class=tableClass", inheritInformalParameters = true)
    260    private Any table;
    261
    262    @Environmental(false)
    263    private FormSupport formSupport;
    264
    265    /**
    266     * Defines where block and label overrides are obtained from. By default, the Grid component provides block
    267     * overrides (from its block parameters).
    268     */
    269    @Parameter(value = "this", allowNull = false)
    270    @Property(write = false)
    271    private PropertyOverrides overrides;
    272
    273    /**
    274     * Set up via the traditional or Ajax component event request handler
    275     */
    276    @Environmental
    277    private ComponentEventResultProcessor componentEventResultProcessor;
    278
    279    @Inject
    280    private ComponentDefaultProvider defaultsProvider;
    281
    282    ValueEncoder defaultEncoder()
    283    {
    284        return defaultsProvider.defaultValueEncoder("row", resources);
    285    }
    286
    287    /**
    288     * A version of GridDataSource that caches the availableRows and empty properties. This addresses TAPESTRY-2245.
    289     */
    290    static class CachingDataSource implements GridDataSource
    291    {
    292        private final GridDataSource delegate;
    293
    294        private boolean availableRowsCached;
    295
    296        private int availableRows;
    297
    298        private boolean emptyCached;
    299
    300        private boolean empty;
    301
    302        CachingDataSource(GridDataSource delegate)
    303        {
    304            this.delegate = delegate;
    305        }
    306
    307        @Override
    308        public boolean isEmpty()
    309        {
    310            if (!emptyCached)
    311            {
    312                empty = delegate.isEmpty();
    313                emptyCached = true;
    314                if (empty)
    315                {
    316                    availableRows = 0;
    317                    availableRowsCached = true;
    318                }
    319            }
    320
    321            return empty;
    322        }
    323
    324        @Override
    325        public int getAvailableRows(int limit)
    326        {
    327            if (!availableRowsCached)
    328            {
    329                int result = delegate.getAvailableRows(limit);
    330                if (result == 0)
    331                {
    332                    empty = true;
    333                    emptyCached = true;
    334                } else {
    335                    empty = false;
    336                    emptyCached = true;
    337                }
    338                if (result < limit) {
    339                    availableRows = result;
    340                    availableRowsCached = true;
    341                }
    342                return result;
    343            } else {
    344              return Math.min(availableRows, limit);
    345            }
    346        }
    347
    348        public int getAvailableRows()
    349        {
    350            if (!availableRowsCached)
    351            {
    352                availableRows = delegate.getAvailableRows();
    353                availableRowsCached = true;
    354                if (availableRows == 0)
    355                {
    356                    empty = true;
    357                    emptyCached = true;
    358                } else {
    359                  empty = false;
    360                  emptyCached = true;
    361              }
    362            }
    363
    364            return availableRows;
    365        }
    366
    367        public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
    368        {
    369            delegate.prepare(startIndex, endIndex, sortConstraints);
    370        }
    371
    372        public Object getRowValue(int index)
    373        {
    374            return delegate.getRowValue(index);
    375        }
    376
    377        public Class getRowType()
    378        {
    379            return delegate.getRowType();
    380        }
    381    }
    382
    383    /**
    384     * Default implementation that only allows a single column to be the sort column, and stores the sort information as
    385     * persistent fields of the Grid component.
    386     */
    387    class DefaultGridSortModel implements GridSortModel
    388    {
    389        public ColumnSort getColumnSort(String columnId)
    390        {
    391            if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId()))
    392            {
    393                return ColumnSort.UNSORTED;
    394            }
    395
    396            return getColumnSort();
    397        }
    398
    399        private ColumnSort getColumnSort()
    400        {
    401            return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
    402        }
    403
    404        public void updateSort(String columnId)
    405        {
    406            assert InternalUtils.isNonBlank(columnId);
    407
    408            if (columnId.equals(paginationModel.getSortColumnId()))
    409            {
    410                setSortAscending(!getSortAscending());
    411                return;
    412            }
    413
    414            paginationModel.setSortColumnId(columnId);
    415            setSortAscending(true);
    416        }
    417
    418        public List<SortConstraint> getSortConstraints()
    419        {
    420            // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null.
    421            if (paginationModel == null || paginationModel.getSortColumnId() == null)
    422            {
    423                return Collections.emptyList();
    424            }
    425
    426            PropertyModel sortModel = getDataModel().getById(paginationModel.getSortColumnId());
    427
    428            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());
    429
    430            return Collections.singletonList(constraint);
    431        }
    432
    433        public void clear()
    434        {
    435            paginationModel.setSortColumnId(null);
    436            paginationModel.setSortAscending(null);
    437        }
    438    }
    439
    440    GridSortModel defaultSortModel()
    441    {
    442        return new DefaultGridSortModel();
    443    }
    444
    445    /**
    446     * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
    447     * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
    448     * a different mechanism. The returning binding is variant (not invariant).
    449     *
    450     * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)
    451     */
    452    protected Binding defaultModel()
    453    {
    454
    455        return new AbstractBinding()
    456        {
    457            public Object get()
    458            {
    459                // Get the default row type from the data source
    460
    461                GridDataSource gridDataSource = source;
    462
    463                Class rowType = gridDataSource.getRowType();
    464
    465                if (renderTableIfEmpty || rowType == null)
    466                    throw new RuntimeException(
    467                            String.format(
    468                                    "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
    469                                    gridDataSource));
    470
    471                // Properties do not have to be read/write
    472
    473                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
    474            }
    475
    476            /**
    477             * Returns false. This may be overkill, but it basically exists because the model is
    478             * inherently mutable and therefore may contain client-specific state and needs to be
    479             * discarded at the end of the request. If the model were immutable, then we could leave
    480             * invariant as true.
    481             */
    482            @Override
    483            public boolean isInvariant()
    484            {
    485                return false;
    486            }
    487        };
    488    }
    489
    490    static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
    491    {
    492        private static final long serialVersionUID = 8545187927995722789L;
    493
    494        public void execute(Grid component)
    495        {
    496            component.setupDataSource();
    497        }
    498
    499        @Override
    500        public String toString()
    501        {
    502            return "Grid.SetupDataSource";
    503        }
    504    };
    505
    506    Object setupRender()
    507    {
    508        if (formSupport != null)
    509        {
    510            formSupport.store(this, SETUP_DATA_SOURCE);
    511        }
    512
    513        setupDataSource();
    514
    515        // If there's no rows, display the empty block placeholder.
    516
    517        return !renderTableIfEmpty && cachingSource.isEmpty() ? empty : null;
    518    }
    519
    520    void cleanupRender()
    521    {
    522        // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper
    523        // zone for each iteration (TAP5-2256)
    524        zone = null;
    525
    526        // If grid is rendered inside a Loop. be sure to generate a new data model for
    527        // each iteration (TAP5-2470)
    528        dataModel = null;
    529    }
    530
    531    public GridPaginationModel getDefaultPaginationModel()
    532    {
    533        if (defaultPaginationModel == null)
    534        {
    535            defaultPaginationModel = new GridPaginationModelImpl();
    536        }
    537
    538        return defaultPaginationModel;
    539    }
    540
    541    void setupDataSource()
    542    {
    543        // TAP5-34: We pass the source into the CachingDataSource now; previously
    544        // we were accessing source directly, but during submit the value wasn't
    545        // cached, and therefore access was very inefficient, and sorting was
    546        // very inconsistent during the processing of the form submission.
    547
    548        int effectiveCurrentPage = getCurrentPage();
    549
    550        int numberOfRowsRequiredToShowCurrentPage = 1 + (effectiveCurrentPage - 1) * rowsPerPage;
    551        int numberOfRowsRequiredToFillCurrentPage = effectiveCurrentPage * rowsPerPage;
    552
    553        cachingSource = new CachingDataSource(source);
    554        if (pagerPosition != GridPagerPosition.NONE)
    555        {
    556            // We're going to render the pager, so we need to determine the total number of rows anyway.
    557            // We do that eagerly here so we don't have to perform two count operations; the subsequent
    558            // ones will return a cached result
    559            cachingSource.getAvailableRows();
    560        }
    561        int availableRowsWithLimit = cachingSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage);
    562
    563        if (availableRowsWithLimit == 0)
    564            return;
    565
    566        // This captures when the number of rows has decreased, typically due to deletions.
    567
    568        if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit)
    569        {
    570            int maxPage = ((availableRowsWithLimit - 1) / rowsPerPage) + 1;
    571            effectiveCurrentPage = maxPage;
    572        }
    573
    574        int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;
    575
    576        int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRowsWithLimit - 1);
    577
    578        cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
    579    }
    580
    581    Object beginRender(MarkupWriter writer)
    582    {
    583        // Skip rendering of component (template, body, etc.) when there's nothing to display.
    584        // The empty placeholder will already have rendered.
    585
    586        if (cachingSource.isEmpty())
    587            return !renderTableIfEmpty ? false : null;
    588
    589        if (inPlace && zone == null)
    590        {
    591            javaScriptSupport.require("t5/core/zone");
    592
    593            writer.element("div", "data-container-type", "zone");
    594
    595            didRenderZoneDiv = true;
    596
    597            // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the
    598            // GridPager and etc.  That's no longer necessary, so zone will always be null or "^".  We don't
    599            // even need any special ids to be allocated!
    600            zone = "^";
    601        }
    602
    603        return null;
    604    }
    605
    606    void afterRender(MarkupWriter writer)
    607    {
    608        if (didRenderZoneDiv)
    609        {
    610            writer.end(); // div
    611            didRenderZoneDiv = false;
    612        }
    613    }
    614
    615    public BeanModel getDataModel()
    616    {
    617        if (dataModel == null)
    618        {
    619            dataModel = model;
    620
    621            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
    622        }
    623
    624        return dataModel;
    625    }
    626
    627    public int getNumberOfProperties()
    628    {
    629        return getDataModel().getPropertyNames().size();
    630    }
    631
    632    public GridDataSource getDataSource()
    633    {
    634        return cachingSource;
    635    }
    636
    637    public GridSortModel getSortModel()
    638    {
    639        return sortModel;
    640    }
    641
    642    public Object getPagerTop()
    643    {
    644        return pagerPosition.isMatchTop() ? pager : null;
    645    }
    646
    647    public Object getPagerBottom()
    648    {
    649        return pagerPosition.isMatchBottom() ? pager : null;
    650    }
    651
    652    public int getCurrentPage()
    653    {
    654        Integer currentPage = paginationModel.getCurrentPage();
    655
    656        return currentPage == null ? 1 : currentPage;
    657    }
    658
    659    public void setCurrentPage(int currentPage)
    660    {
    661        paginationModel.setCurrentPage(currentPage);
    662    }
    663
    664    private boolean getSortAscending()
    665    {
    666        Boolean sortAscending = paginationModel.getSortAscending();
    667
    668        return sortAscending != null && sortAscending.booleanValue();
    669    }
    670
    671    private void setSortAscending(boolean sortAscending)
    672    {
    673        paginationModel.setSortAscending(sortAscending);
    674    }
    675
    676    public int getRowsPerPage()
    677    {
    678        return rowsPerPage;
    679    }
    680
    681    public Object getRow()
    682    {
    683        return row;
    684    }
    685
    686    public void setRow(Object row)
    687    {
    688        this.row = row;
    689    }
    690
    691    /**
    692     * Resets the Grid to inital settings; this sets the current page to one, and
    693     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
    694     */
    695    public void reset()
    696    {
    697        sortModel.clear();
    698        setCurrentPage(1);
    699    }
    700
    701    /**
    702     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
    703     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
    704     * re-render themselves. Invokes
    705     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
    706     * Grid component) as the content provider for the update.
    707     */
    708    void onInPlaceUpdate() throws IOException
    709    {
    710        this.zone = "^";
    711
    712        componentEventResultProcessor.processResultValue(this);
    713    }
    714
    715    public String getClientId()
    716    {
    717        return table.getClientId();
    718    }
    719}