com.tocea.scertify.eclipse.scertifycode.ui.stats.views.GraphStatsView.java Source code

Java tutorial

Introduction

Here is the source code for com.tocea.scertify.eclipse.scertifycode.ui.stats.views.GraphStatsView.java

Source

// ============================================================================
//
// Copyright (C) 2002-2006 David Schneider, Lars Kdderitzsch, Fabrice Bellingard
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// ============================================================================

package com.tocea.scertify.eclipse.scertifycode.ui.stats.views;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import org.eclipse.core.internal.resources.ResourceException;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.MarkerUtilities;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.entity.PieSectionEntity;
import org.jfree.chart.plot.PiePlot3D;
import org.jfree.experimental.chart.swt.ChartComposite;
import org.jfree.util.Rotation;
import org.osgi.service.prefs.BackingStoreException;

import com.tocea.scertify.eclipse.scertifycode.ui.Activator;
import com.tocea.scertify.eclipse.scertifycode.ui.ScertifyUIPluginImages;
import com.tocea.scertify.eclipse.scertifycode.ui.stats.Messages;
import com.tocea.scertify.eclipse.scertifycode.ui.stats.data.MarkerStat;
import com.tocea.scertify.eclipse.scertifycode.ui.stats.data.Stats;
import com.tocea.scertify.eclipse.scertifycode.ui.util.table.EnhancedTableViewer;
import com.tocea.scertify.eclipse.scertifycode.ui.util.table.ITableComparableProvider;
import com.tocea.scertify.eclipse.scertifycode.ui.util.table.ITableSettingsProvider;

/**
 * View that shows a graph for the Checkstyle marker distribution.
 * 
 * @author Fabrice BELLINGARD
 * @author Lars Kdderitzsch
 * @author Tocea
 */

public class GraphStatsView extends AbstractStatsView {

    //
    // constants
    //

    /**
     * Content provider for the detail table viewer.
     * 
     * @author Lars Kdderitzsch
     */
    private class DetailContentProvider implements IStructuredContentProvider {

        private Object[] mCurrentDetails;

        /**
         * @see org.eclipse.jface.viewers.IContentProvider#dispose()
         */

        public void dispose() {

            this.mCurrentDetails = null;
        }

        /**
         * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
         */

        public Object[] getElements(final Object inputElement) {

            if (this.mCurrentDetails == null) {
                // find the marker statistics for the current category
                final Stats currentStats = (Stats) inputElement;
                final Collection markerStats = currentStats.getMarkerStats();
                final Iterator it = markerStats.iterator();
                while (it.hasNext()) {
                    final MarkerStat markerStat = (MarkerStat) it.next();
                    if (markerStat.getIdentifiant().equals(GraphStatsView.this.mCurrentDetailCategory)) {
                        this.mCurrentDetails = markerStat.getMarkers().toArray();
                        break;
                    }
                }
            }

            return this.mCurrentDetails != null ? this.mCurrentDetails : new Object[0];
        }

        /**
         * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object,
         *      java.lang.Object)
         */

        public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) {

            this.mCurrentDetails = null;
        }

    }

    /**
     * Label provider for the detail table viewer.
     * 
     * @author Lars Kdderitzsch
     */
    private class DetailViewMultiProvider extends LabelProvider
            implements ITableLabelProvider, ITableComparableProvider, ITableSettingsProvider {

        /**
         * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnImage(java.lang.Object, int)
         */

        public Image getColumnImage(final Object obj, final int index) {

            Image image = null;
            final IMarker marker = (IMarker) obj;

            if (index == 0) {
                final int severity = MarkerUtilities.getSeverity(marker);
                final ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();

                if (IMarker.SEVERITY_ERROR == severity) {
                    image = sharedImages.getImage(ISharedImages.IMG_OBJS_ERROR_TSK);
                } else if (IMarker.SEVERITY_WARNING == severity) {
                    image = sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK);
                } else if (IMarker.SEVERITY_INFO == severity) {
                    image = sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK);
                }
            }
            return image;
        }

        /**
         * @see org.eclipse.jface.viewers.ITableLabelProvider#getColumnText(java.lang.Object, int)
         */

        public String getColumnText(final Object obj, final int index) {

            final IMarker marker = (IMarker) obj;
            String text = null;

            try {
                switch (index) {
                case 1:
                    text = marker.getResource().getName();
                    break;
                case 2:
                    text = marker.getResource().getParent().getFullPath().toString();
                    break;
                case 3:
                    text = marker.getAttribute(IMarker.LINE_NUMBER).toString();
                    break;
                case 4:
                    text = marker.getAttribute(IMarker.MESSAGE).toString();
                    break;

                default:
                    text = ""; //$NON-NLS-1$
                    break;
                }
            } catch (final ResourceException e) {
                //NOOP Marker not found. probably regenerating
            } catch (final Exception e) {
                // Can't do anything: let's put a default value
                text = Messages.MarkerStatsView_unknownProblem;
                Activator.log(e);
            }

            return text;
        }

        public Comparable getComparableValue(final Object element, final int colIndex) {

            final IMarker marker = (IMarker) element;
            Comparable comparable = null;

            switch (colIndex) {
            case 0:
                comparable = new Integer(marker.getAttribute(IMarker.SEVERITY, Integer.MAX_VALUE) * -1);
                break;
            case 1:
                comparable = marker.getResource().getName();
                break;
            case 2:
                comparable = marker.getResource().getParent().getFullPath().toString();
                break;
            case 3:
                comparable = new Integer(marker.getAttribute(IMarker.LINE_NUMBER, Integer.MAX_VALUE));
                break;
            case 4:
                comparable = marker.getAttribute(IMarker.MESSAGE, "").toString();
                break;

            default:
                comparable = ""; //$NON-NLS-1$
                break;
            }

            return comparable;
        }

        public IDialogSettings getTableSettings() {

            final IDialogSettings mainSettings = getDialogSettings();

            IDialogSettings settings = mainSettings.getSection(TAG_SECTION_DETAIL);

            if (settings == null) {
                settings = mainSettings.addNewSection(TAG_SECTION_DETAIL);
            }

            return settings;
        }
    }

    //
    // attributes
    //

    /** The unique view id. */
    public static final String VIEW_ID = GraphStatsView.class.getName();

    private static final String TAG_SECTION_DETAIL = "detailView";

    /** The label containing the view description. */
    private Label mLabelDesc;

    /** The main composite. */
    private Composite mMainSection;

    /** The stack layout of the main composite. */
    private StackLayout mStackLayout;

    /** The detail viewer. */
    private EnhancedTableViewer mDetailViewer;

    /** The composite to harbor the JFreeChart control. */
    private Composite mMasterComposite;

    /** The graph component. */
    private JFreeChart mGraph;

    /** The dataset for the graph. */
    private GraphPieDataset mPieDataset;

    /** The action to go back to the master view. */
    private Action mDrillBackAction;

    /** Opens the editor and shows the error in the code. */
    private Action mShowErrorAction;

    /** Action to go back to the marker overview view. */
    private Action mListingAction;

    // /** Allows to show all the categories or not. */
    // private Action mShowAllCategoriesAction;

    /** The current violation category to show in details view. */
    private String mCurrentDetailCategory;

    /** The state if the view is currently drilled down to details. */
    private boolean mIsDrilledDown;

    //
    // methods
    //

    /**
     * Cre le graphe JFreeChart.
     * 
     * @param piedataset
     *            : la source de donnes  afficher
     * @return le diagramme
     */
    private JFreeChart createChart(final GraphPieDataset piedataset) {

        final JFreeChart jfreechart = ChartFactory.createPieChart3D(null, piedataset, false, true, false);
        jfreechart.setAntiAlias(true);
        jfreechart.setTextAntiAlias(true);

        final PiePlot3D pieplot3d = (PiePlot3D) jfreechart.getPlot();
        // pieplot3d.setInsets(new RectangleInsets(0, 0, 0, 0));

        final double angle = 290D;
        pieplot3d.setStartAngle(angle);
        pieplot3d.setDirection(Rotation.CLOCKWISE);
        final float foreground = 0.5F;
        pieplot3d.setForegroundAlpha(foreground);
        pieplot3d.setBackgroundAlpha(0.0F);
        pieplot3d.setNoDataMessage(Messages.GraphStatsView_noDataToDisplay);

        pieplot3d.setOutlinePaint(null);
        pieplot3d.setLabelFont(new Font("SansSerif", Font.PLAIN, 10));
        pieplot3d.setLabelGap(0.02);
        pieplot3d.setLabelOutlinePaint(null);
        pieplot3d.setLabelShadowPaint(null);
        pieplot3d.setLabelBackgroundPaint(Color.WHITE);
        pieplot3d.setBackgroundPaint(Color.WHITE);

        pieplot3d.setInteriorGap(0.02);
        pieplot3d.setMaximumLabelWidth(0.20);

        return jfreechart;
    }

    /**
     * Creates the table viewer for the detail view.
     * 
     * @param parent
     *            the parent composite
     * @return the detail table viewer
     */
    private EnhancedTableViewer createDetailView(final Composite parent) {

        // le tableau
        final EnhancedTableViewer detailViewer = new EnhancedTableViewer(parent,
                SWT.H_SCROLL | SWT.V_SCROLL | SWT.SINGLE | SWT.FULL_SELECTION);
        final GridData gridData = new GridData(GridData.FILL_BOTH);
        detailViewer.getControl().setLayoutData(gridData);

        // setup the table columns
        final Table table = detailViewer.getTable();
        table.setLinesVisible(true);
        table.setHeaderVisible(true);

        final TableColumn severityCol = new TableColumn(table, SWT.CENTER, 0);
        severityCol.setWidth(20);
        severityCol.setResizable(false);

        final TableColumn idCol = new TableColumn(table, SWT.LEFT, 1);
        idCol.setText(Messages.MarkerStatsView_fileColumn);
        idCol.setWidth(150);

        final TableColumn folderCol = new TableColumn(table, SWT.LEFT, 2);
        folderCol.setText(Messages.MarkerStatsView_folderColumn);
        folderCol.setWidth(300);

        final TableColumn countCol = new TableColumn(table, SWT.CENTER, 3);
        countCol.setText(Messages.MarkerStatsView_lineColumn);
        countCol.pack();

        final TableColumn messageCol = new TableColumn(table, SWT.LEFT, 4);
        messageCol.setText(Messages.MarkerStatsView_messageColumn);
        messageCol.setWidth(300);

        // set the providers
        detailViewer.setContentProvider(new DetailContentProvider());
        final DetailViewMultiProvider multiProvider = new DetailViewMultiProvider();
        detailViewer.setLabelProvider(multiProvider);
        detailViewer.setTableComparableProvider(multiProvider);
        detailViewer.setTableSettingsProvider(multiProvider);
        detailViewer.installEnhancements();

        // add selection listener to maintain action state
        detailViewer.addSelectionChangedListener(new ISelectionChangedListener() {

            public void selectionChanged(final SelectionChangedEvent event) {

                GraphStatsView.this.updateActions();
            }
        });

        // hooks the action to double click
        hookDoubleClickAction(this.mShowErrorAction, detailViewer);

        // and to the context menu too
        final ArrayList actionList = new ArrayList(1);
        actionList.add(this.mDrillBackAction);
        actionList.add(this.mShowErrorAction);
        actionList.add(new Separator());
        hookContextMenu(actionList, detailViewer);

        return detailViewer;
    }

    /**
     * Creates the master view containing the chart.
     * 
     * @param parent
     *            the parent composite
     * @return the chart composite
     */
    public Composite createMasterView(final Composite parent) {

        // create the date set for the chart
        this.mPieDataset = new GraphPieDataset();
        // this.mPieDataset.setShowAllCategories(this.mShowAllCategoriesAction.isChecked());
        this.mPieDataset.setShowAllCategories(true);
        this.mGraph = createChart(this.mPieDataset);

        // creates the chart component
        // final Composite embeddedComposite = new Composite(parent, SWT.EMBEDDED);
        // embeddedComposite.setLayoutData(new GridData(GridData.FILL_VERTICAL));

        // experimental usage of JFreeChart SWT
        // the composite to harbor the Swing chart control
        ChartComposite embeddedComposite = new ChartComposite(parent, SWT.NONE, mGraph, true);
        embeddedComposite.setLayoutData(new GridData(GridData.FILL_BOTH));

        embeddedComposite.addChartMouseListener(new ChartMouseListener() {

            public void chartMouseClicked(final ChartMouseEvent event) {

                final MouseEvent trigger = event.getTrigger();
                if (trigger.getButton() == MouseEvent.BUTTON1 && event.getEntity() instanceof PieSectionEntity) {

                    // && trigger.getClickCount() == 2 //doubleclick not correctly detected with the SWT composite

                    GraphStatsView.this.mMasterComposite.getDisplay().syncExec(new Runnable() {

                        public void run() {

                            GraphStatsView.this.mIsDrilledDown = true;
                            GraphStatsView.this.mCurrentDetailCategory = (String) ((PieSectionEntity) event
                                    .getEntity()).getSectionKey();
                            GraphStatsView.this.mStackLayout.topControl = GraphStatsView.this.mDetailViewer
                                    .getTable();
                            GraphStatsView.this.mMainSection.layout();
                            GraphStatsView.this.mDetailViewer
                                    .setInput(GraphStatsView.this.mDetailViewer.getInput());

                            GraphStatsView.this.updateActions();
                            GraphStatsView.this.updateLabel();
                        }
                    });
                } else {
                    event.getTrigger().consume();
                }
            }

            public void chartMouseMoved(final ChartMouseEvent event) {

                // NOOP
            }
        });

        return embeddedComposite;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.eclipse.ui.IWorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
     */

    public void createPartControl(final Composite parent) {

        super.createPartControl(parent);

        // set up the main layout
        final GridLayout layout = new GridLayout(1, false);
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        parent.setLayout(layout);

        // the label
        this.mLabelDesc = new Label(parent, SWT.NONE);
        final GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.grabExcessHorizontalSpace = true;
        this.mLabelDesc.setLayoutData(gridData);

        // the main section
        this.mMainSection = new Composite(parent, SWT.NONE);
        this.mStackLayout = new StackLayout();
        this.mStackLayout.marginHeight = 0;
        this.mStackLayout.marginWidth = 0;
        this.mMainSection.setLayout(this.mStackLayout);
        this.mMainSection.setLayoutData(new GridData(GridData.FILL_BOTH));

        // create the master viewer
        this.mMasterComposite = createMasterView(this.mMainSection);

        // create the detail viewer
        this.mDetailViewer = createDetailView(this.mMainSection);

        this.mStackLayout.topControl = this.mMasterComposite;

        updateActions();

        // initialize the view data
        refresh(Job.DECORATE);
    }

    /**
     * {@inheritDoc}
     */

    protected String getViewId() {

        return VIEW_ID;
    }

    /**
     * {@inheritDoc}
     */

    protected void handleStatsRebuilt() {

        if (!this.mMasterComposite.isDisposed()) {
            // change the marker stats on the data set
            this.mPieDataset.setStats(getStats());
            this.mDetailViewer.setInput(getStats());

            // update the actions and the label
            updateActions();
            updateLabel();
        }
    }

    /**
     * Adds the actions to the tableviewer context menu.
     * 
     * @param actions
     *            a collection of IAction objets
     */
    private void hookContextMenu(final Collection actions, final StructuredViewer viewer) {

        final MenuManager menuMgr = new MenuManager();
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener() {

            public void menuAboutToShow(final IMenuManager manager) {

                for (final Iterator iter = actions.iterator(); iter.hasNext();) {
                    final Object item = iter.next();
                    if (item instanceof IContributionItem) {
                        manager.add((IContributionItem) item);
                    } else if (item instanceof IAction) {
                        manager.add((IAction) item);
                    }
                }
                manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
            }
        });
        final Menu menu = menuMgr.createContextMenu(viewer.getControl());
        viewer.getControl().setMenu(menu);

        getSite().registerContextMenu(menuMgr, viewer);
    }

    /**
     * Specifies which action will be run when double clicking on the viewer.
     * 
     * @param action
     *            the IAction to add
     */
    private void hookDoubleClickAction(final IAction action, final StructuredViewer viewer) {

        viewer.addDoubleClickListener(new IDoubleClickListener() {

            public void doubleClick(final DoubleClickEvent event) {

                action.run();
            }
        });
    }

    /**
     * {@inheritDoc}
     */

    protected void initMenu(final IMenuManager menu) {

        // menu.add(new FiltersAction(this));
        // menu.add(new Separator());
        // menu.add(this.mShowAllCategoriesAction);
    }

    /**
     * {@inheritDoc}
     */

    protected void initToolBar(final IToolBarManager tbm) {

        tbm.add(this.mListingAction);
        // tbm.add(new Separator());
        // tbm.add(this.mShowAllCategoriesAction);
        // tbm.add(new FiltersAction(this));
    }

    /**
     * See method below.
     * 
     * @see com.tocea.scertify.eclipse.stats.views.AbstractStatsView#makeActions()
     */

    protected void makeActions() {

        this.mListingAction = new Action() {

            public void run() {

                try {
                    GraphStatsView.this.getSite().getWorkbenchWindow().getActivePage()
                            .showView(MarkerStatsView.VIEW_ID);
                } catch (final PartInitException e) {
                    if (Activator.isLogging()) {
                        Activator.log(e,
                                NLS.bind(Messages.GraphStatsView_unableToOpenListingView, MarkerStatsView.VIEW_ID));
                        // TO DO : mettre message d'erreur  l'utilisateur
                    }
                }
            }
        };
        this.mListingAction.setText(Messages.GraphStatsView_displayListing);
        this.mListingAction.setToolTipText(Messages.GraphStatsView_displayListing);
        this.mListingAction.setImageDescriptor(ScertifyUIPluginImages.LIST_VIEW_ICON);

        // action used to go back to the master view
        this.mDrillBackAction = new Action() {

            public void run() {

                GraphStatsView.this.mIsDrilledDown = false;
                GraphStatsView.this.mCurrentDetailCategory = null;
                GraphStatsView.this.mStackLayout.topControl = GraphStatsView.this.mMasterComposite;
                GraphStatsView.this.mMainSection.layout();

                GraphStatsView.this.updateActions();
                GraphStatsView.this.updateLabel();
            }
        };
        this.mDrillBackAction.setText(Messages.MarkerStatsView_actionBack);
        this.mDrillBackAction.setToolTipText(Messages.MarkerStatsView_actionBackTooltip);
        this.mDrillBackAction.setImageDescriptor(
                PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(ISharedImages.IMG_TOOL_BACK));
        this.mDrillBackAction.setDisabledImageDescriptor(PlatformUI.getWorkbench().getSharedImages()
                .getImageDescriptor(ISharedImages.IMG_TOOL_BACK_DISABLED));

        // action used to show a specific error in the editor
        this.mShowErrorAction = new Action() {

            public void run() {

                final IStructuredSelection selection = (IStructuredSelection) GraphStatsView.this.mDetailViewer
                        .getSelection();
                if (selection.getFirstElement() instanceof IMarker) {
                    final IMarker marker = (IMarker) selection.getFirstElement();
                    try {
                        IDE.openEditor(GraphStatsView.this.getSite().getPage(), marker);
                    } catch (final PartInitException e) {
                        Activator.log(e, Messages.MarkerStatsView_unableToShowMarker);
                        // TO DO : Open information dialog to notify the user
                    }
                }
            }
        };
        this.mShowErrorAction.setText(Messages.MarkerStatsView_displayError);
        this.mShowErrorAction.setToolTipText(Messages.MarkerStatsView_displayErrorTooltip);
        this.mShowErrorAction.setImageDescriptor(
                PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(IDE.SharedImages.IMG_OPEN_MARKER));

    }

    /**
     * Helper method to manage the state of the view's actions.
     */
    private void updateActions() {

        this.mDrillBackAction.setEnabled(this.mIsDrilledDown);
        this.mShowErrorAction.setEnabled(this.mIsDrilledDown && !this.mDetailViewer.getSelection().isEmpty());
    }

    /**
     * Updates the title label.
     */
    private void updateLabel() {

        if (!this.mIsDrilledDown) {

            final Stats stats = getStats();
            if (stats != null) {
                final String text = NLS.bind(Messages.GraphStatsView_lblViewMessage,
                        new Object[] { new Integer(stats.getMarkerCount()),
                                new Integer(stats.getMarkerStats().size()),
                                new Integer(stats.getMarkerCountAll()) });
                this.mLabelDesc.setText(text);
            } else {
                this.mLabelDesc.setText("");
            }
        } else {

            final String text = NLS.bind(Messages.MarkerStatsView_lblDetailMessage, new Object[] {
                    this.mCurrentDetailCategory, new Integer(this.mDetailViewer.getTable().getItemCount()) });
            this.mLabelDesc.setText(text);
        }
    }
}