org.jahia.ajax.gwt.client.widget.job.JobListPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.ajax.gwt.client.widget.job.JobListPanel.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.ajax.gwt.client.widget.job;

import com.allen_sauer.gwt.log.client.Log;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.data.*;
import com.extjs.gxt.ui.client.event.*;
import com.extjs.gxt.ui.client.store.GroupingStore;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.widget.*;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.form.CheckBox;
import com.extjs.gxt.ui.client.widget.form.FormPanel;
import com.extjs.gxt.ui.client.widget.form.TextField;
import com.extjs.gxt.ui.client.widget.grid.*;
import com.extjs.gxt.ui.client.widget.layout.BorderLayout;
import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.layout.FormData;
import com.extjs.gxt.ui.client.widget.menu.CheckMenuItem;
import com.extjs.gxt.ui.client.widget.menu.Menu;
import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.TextBox;
import org.jahia.ajax.gwt.client.core.BaseAsyncCallback;
import org.jahia.ajax.gwt.client.data.job.GWTJahiaJobDetail;
import org.jahia.ajax.gwt.client.messages.Messages;
import org.jahia.ajax.gwt.client.service.content.JahiaContentManagementService;
import org.jahia.ajax.gwt.client.service.content.JahiaContentManagementServiceAsync;
import org.jahia.ajax.gwt.client.util.Formatter;
import org.jahia.ajax.gwt.client.util.icons.StandardIconsProvider;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * Panel that displays a list of background jobs, future, current and past, along with a split -view to display details on a selected job.
 * In the future this UI might also include actions such as deleting a job, etc...
 * <p/>
 * User: loom
 * Date: Sep 21, 2010
 * Time: 12:28:42 PM
 */
public class JobListPanel extends LayoutContainer {

    private FormPanel detailsPanel;

    private List<GWTJahiaJobDetail> selectedItems = null;
    private transient Timer timer;
    private Button deleteButton;
    private PagingToolBar pagingToolBar;
    private static final String STATUS_EXECUTING = "executing";
    public static final String STATUS_ADDED = "added";
    public static final String STATUS_SCHEDULED = "scheduled";
    private int autoRefreshInterval = 10;
    private boolean autoRefreshActivated = false;
    private List<String> activeGroupNames = null;

    private boolean adminMode;

    public JobListPanel(boolean adminMode) {
        super(new BorderLayout());
        this.adminMode = adminMode;
        init();
    }

    private void init() {
        setBorders(false);
        activeGroupNames = new ArrayList<String>();
        activeGroupNames.add("ActionJob");
        activeGroupNames.add("PublicationJob");
        activeGroupNames.add("ImportJob");
        activeGroupNames.add("SitemapJob");
        activeGroupNames.add("RuleJob");
        final JahiaContentManagementServiceAsync service = JahiaContentManagementService.App.getInstance();

        // data proxy
        RpcProxy<PagingLoadResult<GWTJahiaJobDetail>> proxy = new RpcProxy<PagingLoadResult<GWTJahiaJobDetail>>() {
            @Override
            protected void load(Object loadConfig, AsyncCallback<PagingLoadResult<GWTJahiaJobDetail>> callback) {
                if (loadConfig == null) {
                    service.getJobs(0, Integer.MAX_VALUE, null, null, activeGroupNames, callback);
                } else if (loadConfig instanceof BasePagingLoadConfig) {
                    BasePagingLoadConfig pagingLoadConfig = (BasePagingLoadConfig) loadConfig;
                    int limit = pagingLoadConfig.getLimit();
                    int offset = pagingLoadConfig.getOffset();
                    Style.SortDir sortDir = pagingLoadConfig.getSortDir();
                    String sortField = pagingLoadConfig.getSortField();
                    service.getJobs(offset, limit, sortField, sortDir.name(), activeGroupNames, callback);
                } else {
                    callback.onSuccess(
                            new BasePagingLoadResult<GWTJahiaJobDetail>(new ArrayList<GWTJahiaJobDetail>()));
                }
            }
        };

        // tree loader
        final PagingLoader<BasePagingLoadResult<ModelData>> loader = new BasePagingLoader<BasePagingLoadResult<ModelData>>(
                proxy);
        loader.setRemoteSort(true);

        // trees store
        final GroupingStore<GWTJahiaJobDetail> store = new GroupingStore<GWTJahiaJobDetail>(loader);
        store.groupBy("status");

        pagingToolBar = new PagingToolBar(50);
        pagingToolBar.bind(loader);

        List<ColumnConfig> config = new ArrayList<ColumnConfig>();

        ColumnConfig column = new ColumnConfig("creationTime", Messages.get("label.creationTime", "Start date"),
                100);
        column.setDateTimeFormat(Formatter.DEFAULT_DATETIME_FORMAT);
        column.setSortable(false);
        config.add(column);

        column = new ColumnConfig("group", Messages.get("label.type", "Type"), 100);
        column.setSortable(false);
        column.setRenderer(new GridCellRenderer<GWTJahiaJobDetail>() {
            public Object render(GWTJahiaJobDetail jobDetail, String property, ColumnData config, int rowIndex,
                    int colIndex, ListStore<GWTJahiaJobDetail> gwtJahiaJobDetailListStore,
                    Grid<GWTJahiaJobDetail> gwtJahiaJobDetailGrid) {
                return new Label(Messages.get("label." + jobDetail.getGroup() + ".task", jobDetail.getGroup()));
            }
        });
        config.add(column);

        column = new ColumnConfig("description", Messages.get("label.description", "Description"), 100);
        column.setSortable(false);
        config.add(column);

        column = new ColumnConfig("status", Messages.get("label.status", "Status"), 100);
        column.setSortable(false);
        config.add(column);

        column = new ColumnConfig("durationInSeconds", Messages.get("label.duration", "Duration"), 140);
        column.setSortable(false);
        column.setRenderer(new GridCellRenderer<GWTJahiaJobDetail>() {
            public Object render(GWTJahiaJobDetail jobDetail, String property, ColumnData config, int rowIndex,
                    int colIndex, ListStore<GWTJahiaJobDetail> store, Grid<GWTJahiaJobDetail> grid) {
                Long duration = jobDetail.getDuration();
                String display = "-";
                if (duration != null) {
                    if (duration < 1000) {
                        display = duration + " " + Messages.get("label.milliseconds.short", "ms");
                    } else if (duration < 60 * 1000L) {
                        display = ((int) (duration / 1000L)) + " " + Messages.get("label.seconds.short", "sec");
                    } else if (duration < 60 * 60 * 1000L) {
                        display = ((int) (duration / (60 * 1000L))) + " "
                                + Messages.get("label.minutes.short", "min") + " "
                                + (((int) (duration / 1000L)) % 60) + " "
                                + Messages.get("label.seconds.short", "sec");
                    } else {
                        display = ((int) (duration / (60 * 60 * 1000L))) + " "
                                + Messages.get("label.hours.short", "h") + " "
                                + (((int) (duration / 60 * 1000L)) % 60) + " "
                                + Messages.get("label.minutes.short", "min");
                    }
                }
                if (STATUS_EXECUTING.equals(jobDetail.getStatus())) {
                    return new Label(Messages.get("label.executingSince", "Executing since ") + display + "...");
                } else {
                    return new Label(display);
                }
            }
        });
        config.add(column);

        column = new ColumnConfig("user", Messages.get("label.user", "User"), 100);
        column.setSortable(false);
        column.setHidden(true);
        config.add(column);

        column = new ColumnConfig("message", Messages.get("label.message", "Message"), 100);
        column.setSortable(false);
        column.setHidden(true);
        config.add(column);

        column = new ColumnConfig("name", Messages.get("label.name", "Name"), 100);
        column.setSortable(false);
        column.setHidden(true);
        config.add(column);

        final ColumnModel cm = new ColumnModel(config);

        GroupingView view = new GroupingView();
        view.setShowGroupedColumn(false);
        view.setForceFit(true);
        view.setGroupRenderer(new GridGroupRenderer() {
            public String render(GroupColumnData data) {
                String f = cm.getColumnById(data.field).getHeaderHtml();
                String l = data.models.size() == 1 ? Messages.get("label.item", "Item")
                        : Messages.get("label.items", "Items");
                return f + ": " + Messages.get("label.job.status." + data.group) + " (" + data.models.size() + " "
                        + l + ")";
            }
        });

        final Grid<GWTJahiaJobDetail> grid = new Grid<GWTJahiaJobDetail>(store, cm);
        grid.setBorders(true);
        grid.setView(view);
        grid.setAutoExpandColumn("description");
        grid.setTrackMouseOver(false);
        grid.setStateId("jobPagingGrid");
        grid.setStateful(true);
        grid.addListener(Events.Attach, new Listener<GridEvent<GWTJahiaJobDetail>>() {
            public void handleEvent(GridEvent<GWTJahiaJobDetail> be) {
                PagingLoadConfig config = new BasePagingLoadConfig();
                config.setOffset(0);
                config.setLimit(50);

                Map<String, Object> state = grid.getState();
                if (state.containsKey("offset")) {
                    int offset = (Integer) state.get("offset");
                    int limit = (Integer) state.get("limit");
                    config.setOffset(offset);
                    config.setLimit(limit);
                }
                if (state.containsKey("sortField")) {
                    config.setSortField((String) state.get("sortField"));
                    config.setSortDir(Style.SortDir.valueOf((String) state.get("sortDir")));
                }
                loader.load(config);
            }
        });
        grid.setLoadMask(true);
        grid.setBorders(true);
        grid.getSelectionModel().addSelectionChangedListener(new SelectionChangedListener<GWTJahiaJobDetail>() {

            @Override
            public void selectionChanged(
                    SelectionChangedEvent<GWTJahiaJobDetail> gwtJahiaJobDetailSelectionChangedEvent) {
                selectedItems = gwtJahiaJobDetailSelectionChangedEvent.getSelection();
                if (adminMode) {
                    for (GWTJahiaJobDetail jobDetail : selectedItems) {
                        if (STATUS_EXECUTING.equals(jobDetail.getStatus())
                                || STATUS_SCHEDULED.equals(jobDetail.getStatus())) {
                            deleteButton.disable();
                            break;
                        } else {
                            deleteButton.enable();
                        }
                    }
                }
                updateDetails();
            }
        });

        ToolBar topToolBar = new ToolBar();
        Button filterButton = new Button(Messages.get("label.typeFilter", "Type filter"));
        final Menu filterMenu = new Menu();
        service.getAllJobGroupNames(new BaseAsyncCallback<List<String>>() {

            public void onSuccess(List<String> groupNames) {
                for (String groupName : groupNames) {
                    final CheckMenuItem groupActivated = new CheckMenuItem(
                            Messages.get("label." + groupName + ".task", groupName));
                    groupActivated.setStateId(groupName);
                    if (activeGroupNames.contains(groupName)) {
                        groupActivated.setChecked(true);
                    } else {
                        groupActivated.setChecked(false);
                    }
                    groupActivated.addListener(Events.CheckChange, new Listener<MenuEvent>() {

                        public void handleEvent(MenuEvent be) {
                            String groupName = groupActivated.getStateId();
                            if (groupActivated.isChecked()) {
                                if (!activeGroupNames.contains(groupName)) {
                                    activeGroupNames.add(groupName);
                                }
                            } else {
                                activeGroupNames.remove(groupName);
                            }
                            pagingToolBar.refresh();
                        }
                    });
                    filterMenu.add(groupActivated);
                }
            }
        });
        filterButton.setMenu(filterMenu);
        topToolBar.add(filterButton);

        topToolBar.add(new SeparatorToolItem());

        final CheckBox autoRefreshCheckBox = new CheckBox();
        autoRefreshCheckBox.setValue(autoRefreshActivated);
        autoRefreshCheckBox.setBoxLabel(Messages.get("label.autoRefresh", "Auto-refresh every"));
        autoRefreshCheckBox.addListener(Events.Change, new Listener<FieldEvent>() {

            public void handleEvent(FieldEvent be) {
                Boolean autoRefreshEnabled = autoRefreshCheckBox.getValue();
                if (autoRefreshEnabled.booleanValue()) {
                    autoRefreshActivated = true;
                    stopAutoRefresh();
                    startAutoRefresh();
                } else {
                    autoRefreshActivated = false;
                    stopAutoRefresh();
                }
            }
        });
        topToolBar.add(autoRefreshCheckBox);

        final TextBox refreshIntervalTextBox = new TextBox();
        refreshIntervalTextBox.setWidth("3em");
        refreshIntervalTextBox.setText(Integer.toString(autoRefreshInterval));
        refreshIntervalTextBox.addChangeHandler(new ChangeHandler() {

            public void onChange(com.google.gwt.event.dom.client.ChangeEvent event) {
                String refreshTextValue = refreshIntervalTextBox.getText();
                int newRefreshInterval = -1;
                try {
                    newRefreshInterval = Integer.parseInt(refreshTextValue);
                } catch (NumberFormatException nfe) {
                    newRefreshInterval = -1;
                }
                if ((newRefreshInterval >= 5) && (newRefreshInterval <= 120)) {
                    autoRefreshInterval = newRefreshInterval;
                    if (autoRefreshActivated) {
                        stopAutoRefresh();
                        startAutoRefresh();
                    }
                } else {
                    refreshIntervalTextBox.setText(Integer.toString(autoRefreshInterval));
                }
            }
        });
        topToolBar.add(new WidgetComponent(refreshIntervalTextBox));

        Label secondsLabel = new Label(" " + Messages.get("label.seconds", "seconds"));
        topToolBar.add(secondsLabel);

        if (adminMode) {
            topToolBar.add(new SeparatorToolItem());
            deleteButton = new Button(Messages.get("label.delete", "Delete"),
                    StandardIconsProvider.STANDARD_ICONS.delete(), new SelectionListener<ButtonEvent>() {
                        @Override
                        public void componentSelected(ButtonEvent ce) {
                            showDeleteConfirmation(false);
                        }
                    });
            deleteButton.disable();
            topToolBar.add(deleteButton);

            topToolBar.add(new Button(Messages.get("label.deleteCompletedJobs", "Delete all completed"),
                    StandardIconsProvider.STANDARD_ICONS.delete(), new SelectionListener<ButtonEvent>() {
                        @Override
                        public void componentSelected(ButtonEvent ce) {
                            showDeleteConfirmation(true);
                        }
                    }));
        }

        ContentPanel listPanel = new ContentPanel();
        listPanel.setFrame(true);
        listPanel.setCollapsible(false);
        listPanel.setAnimCollapse(false);
        // panel.setIcon(Resources.ICONS.table());
        // panel.setHeadingHtml("");
        listPanel.setHeaderVisible(false);
        listPanel.setLayout(new FitLayout());
        listPanel.add(grid);
        listPanel.setSize(600, 350);
        listPanel.setBottomComponent(pagingToolBar);
        listPanel.setTopComponent(topToolBar);
        grid.getAriaSupport().setLabelledBy(listPanel.getId());
        add(listPanel);

        BorderLayoutData centerData = new BorderLayoutData(Style.LayoutRegion.CENTER);
        add(listPanel, centerData);

        FormPanel detailPanel = new FormPanel();
        detailPanel.setBorders(true);
        detailPanel.setBodyBorder(true);
        detailPanel.setHeaderVisible(true);
        detailPanel.setHeadingHtml(Messages.get("label.detailed", "Details"));
        detailPanel.setScrollMode(Style.Scroll.AUTOY);
        detailPanel.setLabelWidth(100);
        detailsPanel = detailPanel;

        BorderLayoutData southData = new BorderLayoutData(Style.LayoutRegion.SOUTH, 200);
        southData.setSplit(true);
        southData.setCollapsible(true);
        add(detailPanel, southData);

        addListener(Events.Detach, new Listener<ComponentEvent>() {

            public void handleEvent(ComponentEvent be) {
                stopAutoRefresh();
            }

        });
    }

    public void startAutoRefresh() {
        timer = new Timer() {
            public void run() {
                pagingToolBar.refresh();
                schedule(autoRefreshInterval * 1000);
            }
        };
        timer.run();
    }

    public void stopAutoRefresh() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    protected void showDeleteConfirmation(final boolean deleteAllCompleted) {
        final JahiaContentManagementServiceAsync service = JahiaContentManagementService.App.getInstance();
        final Dialog dialog = new Dialog();
        dialog.setModal(true);
        dialog.setHeadingHtml(Messages.get("label.delete", "Delete"));
        dialog.setButtons(Dialog.YESNO);
        dialog.addText(deleteAllCompleted
                ? Messages.get("label.deleteCompletedJobs.confirm",
                        "Do you really want to delete all completed jobs?")
                : Messages.get("label.delete.confirm", "Do you really want to delete the current selection?"));
        dialog.setHideOnButtonClick(true);
        dialog.addListener(Events.Hide, new Listener<WindowEvent>() {
            public void handleEvent(WindowEvent be) {
                if (be.getButtonClicked().getItemId().equalsIgnoreCase(Dialog.YES)) {
                    if (deleteAllCompleted) {
                        service.deleteAllCompletedJobs(new BaseAsyncCallback<Integer>() {
                            public void onApplicationFailure(Throwable caught) {
                                com.google.gwt.user.client.Window
                                        .alert(Messages.get("fm_fail") + "\n" + caught.getLocalizedMessage());
                                Log.error(Messages.get("fm_fail"), caught);
                            }

                            public void onSuccess(Integer result) {
                                MessageBox
                                        .info(Messages.get("label.delete", "Delete"),
                                                Messages.getWithArgs("label.deleteCompletedJobs.success",
                                                        "Deleted {0} completed jobs", new Object[] { result }),
                                                null);
                                if (result != null && result > 0) {
                                    pagingToolBar.refresh();
                                }
                            }
                        });
                    } else {
                        for (GWTJahiaJobDetail jobDetail : selectedItems) {
                            service.deleteJob(jobDetail.getName(), jobDetail.getGroup(),
                                    new BaseAsyncCallback<Boolean>() {

                                        public void onApplicationFailure(Throwable caught) {
                                            com.google.gwt.user.client.Window.alert(
                                                    Messages.get("fm_fail") + "\n" + caught.getLocalizedMessage());
                                            Log.error(Messages.get("fm_fail"), caught);
                                        }

                                        public void onSuccess(Boolean result) {
                                            pagingToolBar.refresh();
                                        }
                                    });
                        }
                    }
                }
            }
        });
        dialog.show();
    }

    public void reloadList(Grid grid) {
        ListStore listStore = grid.getStore();
        PagingLoadConfig config = (PagingLoadConfig) listStore.getLoadConfig();
        Loader loader = listStore.getLoader();
        Map<String, Object> state = grid.getState();
        if (state.containsKey("offset")) {
            int offset = (Integer) state.get("offset");
            int limit = (Integer) state.get("limit");
            config.setOffset(offset);
            config.setLimit(limit);
        }
        if (state.containsKey("sortField")) {
            config.setSortField((String) state.get("sortField"));
            config.setSortDir(Style.SortDir.valueOf((String) state.get("sortDir")));
        }
        loader.load(config);
    }

    public void addDetail(String labelKey, String labelDefaultValue, Object value) {
        if (value != null) {
            TextField textField = new TextField();
            textField.setFieldLabel(Messages.get(labelKey, labelDefaultValue));
            textField.setReadOnly(true);
            if (value instanceof String) {
                textField.setValue(value);
            } else if (value instanceof Date) {
                textField.setValue(org.jahia.ajax.gwt.client.util.Formatter.getFormattedDate((Date) value));
            } else {
                textField.setValue(value.toString());
            }
            detailsPanel.add(textField, new FormData("98%"));
        }
    }

    public void addTimeDetail(String labelKey, String labelDefaultValue, Object value) {
        if (value instanceof Long) {
            Date date = new Date((Long) value);
            addDetail(labelKey, labelDefaultValue, date);
        } else {
            addDetail(labelKey, labelDefaultValue, value);
        }
    }

    public void updateDetails() {

        if (detailsPanel == null) {
            // maybe we clicked before it was created properly ?
            return;
        }

        if (selectedItems == null || selectedItems.size() == 0) {
            return;
        }

        detailsPanel.removeAll();
        if (selectedItems.size() == 1) {
            GWTJahiaJobDetail jobDetail = selectedItems.get(0);

            addDetail("label.description", "Description", jobDetail.getDescription());
            addDetail("label.status", "Status", jobDetail.getStatus());
            StringBuffer paths = new StringBuffer();
            for (String path : jobDetail.getTargetPaths()) {
                paths.append(path);
                paths.append(" ");
            }
            addDetail("label.targetPaths", "Target paths", paths.toString());
            addDetail("label.fileName", "File name", jobDetail.getFileName());
            addDetail("label.name", "Name", jobDetail.getName());
            addDetail("label.creationTime", "Creation time", jobDetail.getCreationTime());
            addDetail("label.user", "User key", jobDetail.getUser());
            addDetail("label.group", "Group", jobDetail.getGroup());
            // addDetail("label.jobLabel", "Job label", jobDetail.getLabel());
            // addDetail("label.className", "Job class", jobDetail.getJobClassName());
            addDetail("label.message", "Message", jobDetail.getMessage());
            addTimeDetail("label.beginTime", "Start time", jobDetail.getBeginTime());
            addTimeDetail("label.endTime", "End time", jobDetail.getEndTime());
            addDetail("label.duration", "Duration", jobDetail.getDuration());
            addDetail("label.locale", "Locale code", jobDetail.getLocale());
            addDetail("label.targetNodeIdentifier", "Target node identifier", jobDetail.getTargetNodeIdentifier());
            addDetail("label.targetAction", "Target action", jobDetail.getTargetAction());
            addDetail("label.targetWorkspace", "Target workspace", jobDetail.getTargetWorkspace());
        } else {
            detailsPanel.add(new HTML("<b>" + Messages.get("label.selectedJobCount", "Number of selected jobs")
                    + " :</b> " + selectedItems.size()));
        }
        detailsPanel.layout();

    }

}