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.ajax.markup.html.form.upload;
018
019import java.util.Formatter;
020
021import org.apache.wicket.Application;
022import org.apache.wicket.IInitializer;
023import org.apache.wicket.MarkupContainer;
024import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
025import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
026import org.apache.wicket.markup.head.CssHeaderItem;
027import org.apache.wicket.markup.head.IHeaderResponse;
028import org.apache.wicket.markup.head.JavaScriptHeaderItem;
029import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
030import org.apache.wicket.markup.html.WebMarkupContainer;
031import org.apache.wicket.markup.html.form.Form;
032import org.apache.wicket.markup.html.form.upload.FileUploadField;
033import org.apache.wicket.markup.html.panel.Panel;
034import org.apache.wicket.model.StringResourceModel;
035import org.apache.wicket.request.resource.CssResourceReference;
036import org.apache.wicket.request.resource.JavaScriptResourceReference;
037import org.apache.wicket.request.resource.ResourceReference;
038import org.apache.wicket.request.resource.SharedResourceReference;
039import org.apache.wicket.resource.CoreLibrariesContributor;
040import org.apache.wicket.util.lang.Args;
041import org.apache.wicket.util.visit.IVisit;
042import org.apache.wicket.util.visit.IVisitor;
043
044/**
045 * A panel to show the progress of an HTTP upload.
046 * <p>
047 * Note: For this to work upload progress monitoring must be enabled in the wicket application.
048 * Example:
049 * 
050 * <pre>
051 * <code>
052 *  public class App extends WebApplication {
053 * 
054 *      &#64;Override
055 *      protected void init() {
056 *              super.init();
057 * 
058 *              &lt;b&gt;getApplicationSettings().setUploadProgressUpdatesEnabled(true);&lt;/b&gt; // &lt;--
059 *      }
060 * }
061 * </code>
062 * </pre>
063 * 
064 * For customizing starting text see {@link #RESOURCE_STARTING}.
065 * 
066 * Implementation detail: Despite being located in an Ajax package, the progress communication is
067 * not done via Ajax but with an IFrame instead due to a bug in Webkit based browsers, see
068 * WICKET-3202.
069 * 
070 * @author Andrew Lombardi
071 */
072public class UploadProgressBar extends Panel
073{
074        /**
075         * Resource key used to retrieve starting message for.
076         * 
077         * Example: UploadProgressBar.starting=Upload starting...
078         */
079        public static final String RESOURCE_STARTING = "UploadProgressBar.starting";
080
081        /**
082         * Initializer for this component; binds static resources.
083         */
084        public final static class ComponentInitializer implements IInitializer
085        {
086                @Override
087                public void init(final Application application)
088                {
089                        // register the upload status resource
090                        application.getSharedResources().add(RESOURCE_NAME, new UploadStatusResource());
091                }
092
093                @Override
094                public String toString()
095                {
096                        return "UploadProgressBar initializer";
097                }
098
099                @Override
100                public void destroy(final Application application)
101                {
102                }
103        }
104
105        private static final ResourceReference JS = new JavaScriptResourceReference(
106                UploadProgressBar.class, "progressbar.js");
107
108        private static final ResourceReference CSS = new CssResourceReference(
109                UploadProgressBar.class, "UploadProgressBar.css");
110
111        private static final String RESOURCE_NAME = UploadProgressBar.class.getName();
112
113        private static final long serialVersionUID = 1L;
114
115        private Form<?> form;
116
117        private MarkupContainer statusDiv;
118
119        private MarkupContainer barDiv;
120
121        private final FileUploadField uploadField;
122
123        /**
124         * Constructor that will display the upload progress bar for every submit of the given form.
125         *
126         * @param id
127         *            component id (not null)
128         * @param uploadField
129         *            the file upload field to check for a file upload, or null to display the upload
130         *            field for every submit of the given form
131         */
132        public UploadProgressBar(final String id,  final FileUploadField uploadField)
133        {
134                super(id);
135
136                this.uploadField = uploadField;
137                if (uploadField != null)
138                {
139                        uploadField.setOutputMarkupId(true);
140                }
141
142                setRenderBodyOnly(true);
143        }
144
145
146
147        /**
148         * Constructor that will display the upload progress bar for every submit of the given form.
149         * 
150         * @param id
151         *            component id (not null)
152         * @param form
153         *            form that will be submitted (not null)
154         */
155        public UploadProgressBar(final String id, final Form<?> form)
156        {
157                this(id, form, null);
158        }
159
160        /**
161         * Constructor that will display the upload progress bar for submissions of the given form, that
162         * include a file upload in the given file upload field; i.e. if the user did not select a file
163         * in the given file upload field, the progess bar is not displayed.
164         * 
165         * @param id
166         *            component id (not null)
167         * @param form
168         *            form that is submitted (not null)
169         * @param uploadField
170         *            the file upload field to check for a file upload, or null to display the upload
171         *            field for every submit of the given form
172         */
173        public UploadProgressBar(final String id, final Form<?> form, final FileUploadField uploadField)
174        {
175                super(id);
176
177                this.uploadField = uploadField;
178                if (uploadField != null)
179                {
180                        uploadField.setOutputMarkupId(true);
181                }
182
183                this.form = Args.notNull(form, "form");
184                form.setOutputMarkupId(true);
185
186                setRenderBodyOnly(true);
187        }
188
189        @Override
190        protected void onInitialize()
191        {
192                super.onInitialize();
193                Form<?> form = getCallbackForm();
194                if (form != null) {
195                        form.setOutputMarkupId(true);
196                }
197
198                barDiv = newBarComponent("bar");
199                add(barDiv);
200
201                statusDiv = newStatusComponent("status");
202                add(statusDiv);
203        }
204
205        /**
206         * Creates a component for the status text
207         *
208         * @param id
209         *          The component id
210         * @return the status component
211         */
212        protected MarkupContainer newStatusComponent(String id)
213        {
214                WebMarkupContainer status = new WebMarkupContainer(id);
215                status.setOutputMarkupId(true);
216                return status;
217        }
218
219        /**
220         * Creates a component for the bar
221         *
222         * @param id
223         *          The component id
224         * @return the bar component
225         */
226        protected MarkupContainer newBarComponent(String id)
227        {
228                WebMarkupContainer bar = new WebMarkupContainer(id);
229                bar.setOutputMarkupId(true);
230                return bar;
231        }
232
233        /**
234         * Override this to provide your own CSS, or return null to avoid including the default.
235         * 
236         * @return ResourceReference for your CSS.
237         */
238        protected ResourceReference getCss()
239        {
240                return CSS;
241        }
242
243        /**
244         * {@inheritDoc}
245         */
246        @Override
247        public void renderHead(final IHeaderResponse response)
248        {
249                super.renderHead(response);
250
251                CoreLibrariesContributor.contributeAjax(getApplication(), response);
252                response.render(JavaScriptHeaderItem.forReference(JS));
253                ResourceReference css = getCss();
254                if (css != null)
255                {
256                        response.render(CssHeaderItem.forReference(css));
257                }
258
259                ResourceReference ref = new SharedResourceReference(RESOURCE_NAME);
260
261                final String uploadFieldId = (uploadField == null) ? "null" : ("'" + uploadField.getMarkupId() + "'");
262
263                final String status = new StringResourceModel(RESOURCE_STARTING, this, null).getString();
264
265                CharSequence url = form != null ? urlFor(ref, UploadStatusResource.newParameter(getPage().getId())) :
266                                urlFor(ref, UploadStatusResource.newParameter(uploadField.getMarkupId()));
267
268                StringBuilder builder = new StringBuilder(128);
269                Formatter formatter = new Formatter(builder);
270
271                Form<?> form = getCallbackForm();
272
273                formatter.format(getVarName() + " = new Wicket.WUPB(%s, '%s', '%s', '%s', %s, '%s', %s);",
274                                form != null ? "'" + form.getMarkupId() + "'" : "null", statusDiv.getMarkupId(), barDiv.getMarkupId(), url, uploadFieldId,
275                        status, getOnProgressUpdatedCallBack());
276                response.render(OnDomReadyHeaderItem.forScript(builder.toString()));
277        }
278
279        /**
280         * Allows to pass a JavaScript function that is called when progress in updated.
281         *
282         * @return A JavaScript function.
283         */
284        protected String getOnProgressUpdatedCallBack()
285        {
286                return "function(percent) {}";
287        }
288
289        private String getVarName() {
290                return "window.upb_" + barDiv.getMarkupId();
291        }
292
293
294        public void start(IPartialPageRequestHandler handler)
295        {
296                handler.appendJavaScript(getVarName() + ".start();");
297        }
298
299        /**
300         * Form on where will be installed the JavaScript callback to present the progress bar.
301         * {@link ModalWindow} is designed to hold nested forms and the progress bar callback JavaScript
302         * needs to be add at the form inside the {@link ModalWindow} if one is used.
303         * 
304         * @return form
305         */
306        private Form<?> getCallbackForm()
307        {
308                if (form == null)
309                {
310                        return null;
311                }
312                Boolean insideModal = form.visitParents(ModalWindow.class,
313                        new IVisitor<ModalWindow, Boolean>()
314                        {
315                                @Override
316                                public void component(final ModalWindow object, final IVisit<Boolean> visit)
317                                {
318                                        visit.stop(true);
319                                }
320                        });
321                if ((insideModal != null) && insideModal)
322                {
323                        return form;
324                }
325                else
326                {
327                        return form.getRootForm();
328                }
329        }
330}