001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.extensions.markup.html.repeater.data.table.export;
018
019import java.io.IOException;
020import java.io.OutputStream;
021import java.time.Duration;
022import java.util.LinkedList;
023import java.util.List;
024import org.apache.wicket.AttributeModifier;
025import org.apache.wicket.Component;
026import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractToolbar;
027import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
028import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
029import org.apache.wicket.markup.html.WebMarkupContainer;
030import org.apache.wicket.markup.html.basic.Label;
031import org.apache.wicket.markup.html.link.ResourceLink;
032import org.apache.wicket.markup.repeater.RepeatingView;
033import org.apache.wicket.markup.repeater.data.IDataProvider;
034import org.apache.wicket.model.IModel;
035import org.apache.wicket.model.ResourceModel;
036import org.apache.wicket.request.resource.IResource;
037import org.apache.wicket.request.resource.ResourceStreamResource;
038import org.apache.wicket.util.lang.Args;
039import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
040import org.apache.wicket.util.resource.IResourceStream;
041import org.apache.wicket.util.resource.IResourceStreamWriter;
042
043/**
044 * A toolbar that provides links to download the data represented by all {@link IExportableColumn}s in the table
045 * exported to formats supported by the {@link IDataExporter}s configured.
046 *
047 * @author Jesse Long
048 * @see IDataExporter
049 * @see IExportableColumn
050 */
051public class ExportToolbar extends AbstractToolbar
052{
053        private static final long serialVersionUID = 1L;
054
055        private static final IModel<String> DEFAULT_MESSAGE_MODEL = new ResourceModel(
056                "datatable.export-to");
057
058        private static final IModel<String> DEFAULT_FILE_NAME_MODEL = new ResourceModel(
059                "datatable.export-file-name");
060
061        private final List<IDataExporter> dataExporters = new LinkedList<>();
062
063        private IModel<String> messageModel;
064
065        private IModel<String> fileNameModel;
066
067        /**
068        * Creates a new instance with the default message model. This instance will use "export." as the exported
069        * file name prefix.
070         *
071        * @param table
072         *      The data table this toolbar belongs to.
073        */
074        public ExportToolbar(final DataTable<?, ?> table)
075        {
076                this(table, DEFAULT_MESSAGE_MODEL, DEFAULT_FILE_NAME_MODEL);
077        }
078
079        /**
080         * Creates a new instance with the provided data table and file name model.
081         *
082         * @param table
083         *      The table to which this toolbar belongs.
084         * @param fileNameModel
085         *      The model of the file name. This should exclude the file extensions.
086         */
087        public ExportToolbar(DataTable<?, ?> table, IModel<String> fileNameModel)
088        {
089                this(table, DEFAULT_MESSAGE_MODEL, fileNameModel);
090        }
091
092        /**
093         * Creates a new instance.
094         *
095         * @param table
096         *      The table to which this toolbar belongs.
097         * @param messageModel
098         *      The model of the export message.
099         * @param fileNameModel
100         *      The model of the file name. This should exclude the file extensions.
101         */
102        public ExportToolbar(DataTable<?, ?> table, IModel<String> messageModel, IModel<String> fileNameModel)
103        {
104                super(table);
105                
106                setMessageModel(messageModel);
107                setFileNameModel(fileNameModel);
108        }
109
110        /**
111         * Sets the models of the export message displayed in the toolbar.
112         *
113         * @param messageModel
114         *      the models of the export message displayed in the toolbar.
115         * @return {@code this}, for chaining.
116         */
117        public ExportToolbar setMessageModel(IModel<String> messageModel)
118        {
119                this.messageModel = wrap(Args.notNull(messageModel, "messageModel"));
120                return this;
121        }
122
123        /**
124         * Sets the model of the file name used for the exported data.
125         *
126         * @param fileNameModel
127         *      The model of the file name used for the exported data.
128         * @return {@code this}, for chaining.
129         */
130        public ExportToolbar setFileNameModel(IModel<String> fileNameModel)
131        {
132                this.fileNameModel = wrap(Args.notNull(fileNameModel, "fileNameModel"));
133                return this;
134        }
135
136        /**
137         * Returns the model of the file name used for the exported data.
138         *
139         * @return the model of the file name used for the exported data.
140         */
141        public IModel<String> getFileNameModel()
142        {
143                return fileNameModel;
144        }
145
146        /**
147         * Returns the model of the export message displayed in the toolbar.
148         *
149         * @return the model of the export message displayed in the toolbar.
150         */
151        public IModel<String> getMessageModel()
152        {
153                return messageModel;
154        }
155
156        /**
157         * {@inheritDoc }
158         */
159        @Override
160        protected void onInitialize()
161        {
162                super.onInitialize();
163
164                WebMarkupContainer td = new WebMarkupContainer("td");
165                add(td);
166
167                td.add(AttributeModifier.replace("colspan", new IModel<String>()
168                {
169                        private static final long serialVersionUID = 1L;
170
171                        @Override
172                        public String getObject()
173                        {
174                                return String.valueOf(getTable().getColumns().size()).intern();
175                        }
176                }));
177
178                td.add(new Label("exportTo", messageModel));
179
180                RepeatingView linkContainers = new RepeatingView("linkContainer");
181                td.add(linkContainers);
182
183                for (IDataExporter exporter : dataExporters)
184                {
185                        WebMarkupContainer span = new WebMarkupContainer(linkContainers.newChildId());
186                        linkContainers.add(span);
187
188                        span.add(createExportLink("exportLink", exporter));
189                }
190        }
191
192        /**
193         * Creates a new link to the exported data for the provided {@link IDataExporter}.
194         *
195         * @param componentId
196         *      The component of the link.
197         * @param dataExporter
198         *      The data exporter to use to export the data.
199         * @return a new link to the exported data for the provided {@link IDataExporter}.
200         */
201        protected Component createExportLink(String componentId, final IDataExporter dataExporter)
202        {
203                IResource resource = new ResourceStreamResource()
204                {
205                        /**
206                         * Set fileName and cacheDuration lazily
207                         */
208                        public void respond(Attributes attributes) {
209                                setFileName(fileNameModel.getObject() + "." + dataExporter.getFileNameExtension());
210                                setCacheDuration(ExportToolbar.this.getCacheDuration());
211                                
212                                super.respond(attributes);
213                        }
214                        
215                        @Override
216                        protected IResourceStream getResourceStream(Attributes attributes)
217                        {
218                                return new DataExportResourceStreamWriter(dataExporter, getTable());
219                        }
220                };
221
222                return new ResourceLink<Void>(componentId, resource)
223                        .setBody(dataExporter.getDataFormatNameModel());
224        }
225
226        /**
227         * How long should the export be cached.
228         * 
229         * @return default is {@link Duration#ZERO}
230         */
231        protected Duration getCacheDuration() {
232                return Duration.ZERO;
233        }
234
235        @Override
236        protected void onConfigure()
237        {
238                super.onConfigure();
239
240                calculateVisibility();
241        }
242
243        /**
244         * This toolbar is only visible if there are rows in the data set and if there are exportable columns in the
245         * data table and if there are data exporters added to the toolbar.
246         */
247        protected void calculateVisibility()
248        {
249                final boolean isVisible;
250                if (dataExporters.isEmpty())
251                {
252                        isVisible = false;
253                }
254                else if (getTable().getRowCount() == 0)
255                {
256                        isVisible = false;
257                }
258                else
259                {
260                        boolean foundExportableColumn = false;
261                        for (IColumn<?, ?> col : getTable().getColumns())
262                        {
263                                if (col instanceof IExportableColumn)
264                                {
265                                        foundExportableColumn = true;
266                                        break;
267                                }
268                        }
269                        isVisible = foundExportableColumn;
270                }
271
272                setVisible(isVisible);
273        }
274
275        @Override
276        protected void onDetach()
277        {
278                fileNameModel.detach();
279                messageModel.detach();
280                super.onDetach();
281        }
282
283        /**
284         * Adds a {@link IDataExporter} to the list of data exporters to be used in this toolbar.
285         *
286         * @param exporter
287         *      The {@link IDataExporter} to add to the toolbar.
288         * @return {@code this}, for chaining.
289         */
290        public ExportToolbar addDataExporter(IDataExporter exporter)
291        {
292                Args.notNull(exporter, "exporter");
293                dataExporters.add(exporter);
294                return this;
295        }
296
297        /**
298         * An {@link IResourceStreamWriter} which writes the exportable data from a table to an output stream.
299         */
300        public static class DataExportResourceStreamWriter extends AbstractResourceStreamWriter
301        {
302                private final IDataExporter dataExporter;
303
304                private final DataTable<?, ?> dataTable;
305
306                /**
307                 * Creates a new instance using the provided {@link IDataExporter} to export data.
308                 *
309                 * @param dataExporter
310                 *      The {@link IDataExporter} to use to export data.
311                 * @param dataTable
312                 *      The {@link DataTable} from which to export.
313                 */
314                public DataExportResourceStreamWriter(IDataExporter dataExporter, DataTable<?, ?> dataTable)
315                {
316                        this.dataExporter = dataExporter;
317                        this.dataTable = dataTable;
318                }
319
320                /**
321                 * Writes the exported data to the output stream. This implementation calls
322                 * {@link #exportData(org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable, org.apache.wicket.extensions.markup.html.repeater.data.table.export.IDataExporter, java.io.OutputStream) }.
323                 *
324                 * @param output
325                 *      The output stream to which to export the data.
326                 * @throws IOException if an error occurs.
327                 */
328                @Override
329                public void write(OutputStream output)
330                        throws IOException
331                {
332                        exportData(dataTable, dataExporter, output);
333                }
334
335                /**
336                 * {@inheritDoc}
337                 * <p>
338                 * This method returns the content type returned by {@link IDataExporter#getContentType()}.
339                 *
340                 * @return the content type returned by {@link IDataExporter#getContentType()}.
341                 */
342                @Override
343                public String getContentType()
344                {
345                        return dataExporter.getContentType();
346                }
347
348                /**
349                 * Exports the data from the provided data table to the provided output stream.
350                 * This methods calls {@link IDataExporter#exportData(org.apache.wicket.markup.repeater.data.IDataProvider, java.util.List, java.io.OutputStream) }
351                 * passing it the {@link IDataProvider} of the {@link DataTable}, and a list of the {@link IExportableColumn}s in the table.
352                 *
353                 * @param <T>
354                 *      The type of each row in the data table.
355                 * @param <S>
356                 *      The type of the sort property of the table.
357                 * @param dataTable
358                 *      The {@link DataTable} to export.
359                 * @param dataExporter
360                 *      The {@link IDataExporter} to use to export the data.
361                 * @param outputStream
362                 *      The {@link OutputStream} to which the data should be exported to.
363                 * @throws IOException
364                 */
365                private <T, S> void exportData(DataTable<T, S> dataTable, IDataExporter dataExporter, OutputStream outputStream)
366                        throws IOException
367                {
368                        IDataProvider<T> dataProvider = dataTable.getDataProvider();
369                        List<IExportableColumn<T, ?>> exportableColumns = new LinkedList<>();
370                        for (IColumn<T, S> col : dataTable.getColumns())
371                        {
372                                if (col instanceof IExportableColumn)
373                                {
374                                        exportableColumns.add((IExportableColumn<T, ?>)col);
375                                }
376                        }
377                        dataExporter.exportData(dataProvider, exportableColumns, outputStream);
378                }
379        }
380}