001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ui.client;
029
030import org.opencms.ade.upload.client.I_CmsUploadContext;
031import org.opencms.file.CmsResource;
032import org.opencms.gwt.client.CmsCoreProvider;
033import org.opencms.gwt.client.Messages;
034import org.opencms.gwt.client.rpc.CmsRpcAction;
035import org.opencms.gwt.client.ui.CmsErrorDialog;
036import org.opencms.gwt.client.ui.CmsListItemWidget;
037import org.opencms.gwt.client.ui.CmsPopup;
038import org.opencms.gwt.client.ui.CmsPushButton;
039import org.opencms.gwt.client.ui.CmsVirusReport;
040import org.opencms.gwt.client.ui.I_CmsButton;
041import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
042import org.opencms.gwt.client.ui.input.upload.CmsFileInfo;
043import org.opencms.gwt.client.ui.input.upload.CmsFileInput;
044import org.opencms.gwt.client.ui.input.upload.CmsUploadButton;
045import org.opencms.gwt.client.ui.input.upload.CmsUploadProgressInfo;
046import org.opencms.gwt.client.ui.input.upload.CmsUploader;
047import org.opencms.gwt.client.ui.input.upload.I_CmsUploadDialog;
048import org.opencms.gwt.client.ui.replace.CmsReplaceContentWidget;
049import org.opencms.gwt.shared.CmsListInfoBean;
050import org.opencms.gwt.shared.CmsUploadProgessInfo;
051import org.opencms.gwt.shared.I_CmsUploadConstants;
052import org.opencms.gwt.shared.rpc.I_CmsUploadService;
053import org.opencms.gwt.shared.rpc.I_CmsUploadServiceAsync;
054import org.opencms.util.CmsStringUtil;
055
056import java.util.ArrayList;
057import java.util.Collections;
058import java.util.List;
059import java.util.Map;
060
061import com.google.gwt.core.client.GWT;
062import com.google.gwt.event.dom.client.ClickEvent;
063import com.google.gwt.event.dom.client.ClickHandler;
064import com.google.gwt.event.logical.shared.CloseHandler;
065import com.google.gwt.event.shared.HandlerRegistration;
066import com.google.gwt.json.client.JSONArray;
067import com.google.gwt.json.client.JSONObject;
068import com.google.gwt.json.client.JSONParser;
069import com.google.gwt.json.client.JSONString;
070import com.google.gwt.json.client.JSONValue;
071import com.google.gwt.user.client.Command;
072import com.google.gwt.user.client.Timer;
073import com.google.gwt.user.client.rpc.ServiceDefTarget;
074import com.google.gwt.user.client.ui.PopupPanel;
075import com.google.gwt.user.client.ui.RootPanel;
076
077/**
078 * The single file upload dialog.<p>
079 */
080public class CmsSingleFileUploadDialog extends CmsPopup implements I_CmsUploadDialog {
081
082    /** The upload context. */
083    protected I_CmsUploadContext m_context;
084
085    /** The main content panel. */
086    protected CmsReplaceContentWidget m_mainPanel;
087
088    /** Signals that the upload dialog was canceled. */
089    private boolean m_canceled;
090
091    /** Flag indicating the client is waiting for a server response. */
092    private boolean m_clientLoading;
093
094    /** The close handler. */
095    private CloseHandler<PopupPanel> m_closeHandler;
096
097    /** The sum of all file sizes. */
098    private long m_contentLength;
099
100    /** The current file input. */
101    private CmsFileInput m_fileInput;
102
103    /** The file widget. */
104    private CmsListItemWidget m_fileWidget;
105
106    /** The action to execute when the upload dialog is finished. */
107    private Runnable m_finishAction;
108
109    /** The dialog handler. */
110    private CmsSingleFileUploadHandler m_handler;
111
112    /** The close handler registration. */
113    private HandlerRegistration m_handlerReg;
114
115    /** The loading timer. */
116    private Timer m_loadingTimer;
117
118    /** The OK button. */
119    private CmsPushButton m_okButton;
120
121    /** The upload progress widget. */
122    private CmsUploadProgressInfo m_progressInfo;
123
124    /** The progress timer. */
125    private Timer m_updateProgressTimer;
126
127    /** The upload button. */
128    private CmsUploadButton m_uploadButton;
129
130    /** The upload service. */
131    private I_CmsUploadServiceAsync m_uploadService;
132
133    /**
134     * Constructor.<p>
135     *
136     * @param handler the dialog handler
137     * @param dialogTitle the dialog title
138     */
139    public CmsSingleFileUploadDialog(CmsSingleFileUploadHandler handler, String dialogTitle) {
140
141        super(dialogTitle);
142        m_handler = handler;
143        setModal(true);
144        setGlassEnabled(true);
145        catchNotifications();
146        // create the main panel
147        m_mainPanel = new CmsReplaceContentWidget();
148        // set the main panel as content of the popup
149        setMainContent(m_mainPanel);
150        addDialogClose(new Command() {
151
152            public void execute() {
153
154                cancelReplace();
155            }
156        });
157
158        CmsPushButton cancelButton = createButton(
159            org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_CANCEL_0));
160        cancelButton.addClickHandler(new ClickHandler() {
161
162            /**
163             * @see com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event.dom.client.ClickEvent)
164             */
165            public void onClick(ClickEvent event) {
166
167                cancelReplace();
168            }
169        });
170        addButton(cancelButton);
171        createButtons();
172    }
173
174    /**
175     * @see org.opencms.gwt.client.ui.CmsPopup#addCloseHandler(com.google.gwt.event.logical.shared.CloseHandler)
176     */
177    @Override
178    public HandlerRegistration addCloseHandler(CloseHandler<PopupPanel> handler) {
179
180        m_closeHandler = handler;
181        m_handlerReg = super.addCloseHandler(handler);
182        return m_handlerReg;
183    }
184
185    /**
186     * Returns the action which should be executed when the upload dialog is finished.<p>
187     *
188     * @return an action to run when the upload dialog is finished
189     */
190    public Runnable getFinishAction() {
191
192        return m_finishAction;
193    }
194
195    /**
196     * @see org.opencms.gwt.client.ui.CmsPopup#hide()
197     */
198    @Override
199    public void hide() {
200
201        if ((m_fileInput != null) && RootPanel.get().getElement().isOrHasChild(m_fileInput.getElement())) {
202            m_fileInput.removeFromParent();
203        }
204        super.hide();
205    }
206
207    /**
208     * Parses the upload response of the server and decides what to do.<p>
209     *
210     * @param results a JSON Object
211     */
212    @SuppressWarnings("null")
213    public void parseResponse(String results) {
214
215        cancelUpdateProgress();
216        stopLoadingAnimation();
217
218        if ((!m_canceled) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(results)) {
219            JSONObject jsonObject = JSONParser.parseStrict(results).isObject();
220            boolean success = jsonObject.get(I_CmsUploadConstants.KEY_SUCCESS).isBoolean().booleanValue();
221            // If the upload is done so fast that we did not receive any progress information, then
222            // the content length is unknown. For that reason take the request size to show how
223            // much bytes were uploaded.
224            double size = jsonObject.get(I_CmsUploadConstants.KEY_REQUEST_SIZE).isNumber().doubleValue();
225            long requestSize = Double.valueOf(size).longValue();
226            if (m_contentLength == 0) {
227                m_contentLength = requestSize;
228            }
229            if (success) {
230                m_mainPanel.displayDialogInfo(Messages.get().key(Messages.GUI_UPLOAD_INFO_FINISHING_0), false);
231                m_progressInfo.finish();
232                Map<String, List<String>> viruses = CmsVirusReport.getVirusWarnings(jsonObject);
233                if (viruses.isEmpty()) {
234                    finishUpload(jsonObject);
235                    closeOnSuccess();
236                } else {
237                    hide();
238                    CmsPopup popup = CmsVirusReport.createPopup(viruses, () -> {
239                        finishUpload(jsonObject);
240                    });
241                    popup.center();
242                }
243            } else {
244                String message = jsonObject.get(I_CmsUploadConstants.KEY_MESSAGE).isString().stringValue();
245                String stacktrace = jsonObject.get(I_CmsUploadConstants.KEY_STACKTRACE).isString().stringValue();
246                showErrorReport(message, stacktrace);
247            }
248        }
249    }
250
251    /**
252     * Sets the upload context.<p>
253     *
254     * @param context the new upload context
255     */
256    public void setContext(I_CmsUploadContext context) {
257
258        m_context = context;
259    }
260
261    /**
262     * Sets an action that should be executed if the upload dialog is finished.<p>
263     *
264     * @param action the action to execute when finished
265     */
266    public void setFinishAction(Runnable action) {
267
268        m_finishAction = action;
269    }
270
271    /**
272     * Shows the error report.<p>
273     *
274     * @param message the message to show
275     * @param stacktrace the stacktrace to show
276     */
277    public void showErrorReport(final String message, final String stacktrace) {
278
279        if (!m_canceled) {
280            CmsErrorDialog errDialog = new CmsErrorDialog(message, stacktrace);
281            if (m_handlerReg != null) {
282                m_handlerReg.removeHandler();
283            }
284            if (m_closeHandler != null) {
285                errDialog.addCloseHandler(m_closeHandler);
286            }
287            hide();
288            errDialog.center();
289        }
290    }
291
292    /**
293     * Cancels the replace process.<p>
294     */
295    protected void cancelReplace() {
296
297        m_canceled = true;
298        if (m_progressInfo != null) {
299            cancelUpdateProgress();
300            CmsRpcAction<Boolean> callback = new CmsRpcAction<Boolean>() {
301
302                /**
303                 * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
304                 */
305                @Override
306                public void execute() {
307
308                    getUploadService().cancelUpload(this);
309                }
310
311                /**
312                 * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
313                 */
314                @Override
315                protected void onResponse(Boolean result) {
316
317                    hide();
318                }
319            };
320            callback.execute();
321        } else {
322            hide();
323        }
324    }
325
326    /**
327     * Cancels the upload progress timer.<p>
328     */
329    protected void cancelUpdateProgress() {
330
331        if (m_updateProgressTimer != null) {
332            m_updateProgressTimer.cancel();
333        }
334    }
335
336    /**
337     * Returns the resource type name for a given filename.<p>
338     *
339     * @param file the file info
340     *
341     * @return the resource type name
342     */
343    protected String getResourceType(CmsFileInfo file) {
344
345        return CmsCoreProvider.get().getResourceType(file);
346    }
347
348    /**
349     * Returns the upload service instance.<p>
350     *
351     * @return the upload service instance
352     */
353    protected I_CmsUploadServiceAsync getUploadService() {
354
355        if (m_uploadService == null) {
356            m_uploadService = GWT.create(I_CmsUploadService.class);
357            String serviceUrl = CmsCoreProvider.get().link("org.opencms.ade.upload.CmsUploadService.gwt");
358            ((ServiceDefTarget)m_uploadService).setServiceEntryPoint(serviceUrl);
359        }
360        return m_uploadService;
361    }
362
363    /**
364     * Returns the upload JSP uri.<p>
365     *
366     * @return the upload JSP uri
367     */
368    protected String getUploadUri() {
369
370        return CmsCoreProvider.get().link(I_CmsUploadConstants.UPLOAD_ACTION_JSP_URI);
371    }
372
373    /**
374     * Sets the file input.<p>
375     *
376     * @param fileInput the file input
377     */
378    protected void setFileInput(CmsFileInput fileInput) {
379
380        // only accept file inputs with a single selected file
381        if (fileInput.getFiles().length == 1) {
382            if (m_okButton != null) {
383                m_okButton.enable();
384            }
385            if (m_fileInput != null) {
386                m_fileInput.removeFromParent();
387            }
388            m_fileInput = fileInput;
389
390            RootPanel.get().add(m_fileInput);
391            m_mainPanel.setContainerWidget(createFileWidget(m_fileInput.getFiles()[0]));
392        }
393    }
394
395    /**
396     * Retrieves the progress information from the server.<p>
397     */
398    protected void updateProgress() {
399
400        CmsRpcAction<CmsUploadProgessInfo> callback = new CmsRpcAction<CmsUploadProgessInfo>() {
401
402            /**
403             * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
404             */
405            @Override
406            public void execute() {
407
408                getUploadService().getUploadProgressInfo(this);
409            }
410
411            /**
412             * @see org.opencms.gwt.client.rpc.CmsRpcAction#onFailure(java.lang.Throwable)
413             */
414            @Override
415            public void onFailure(Throwable t) {
416
417                super.onFailure(t);
418                cancelUpdateProgress();
419            }
420
421            /**
422             * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
423             */
424            @Override
425            protected void onResponse(CmsUploadProgessInfo result) {
426
427                updateProgressBar(result);
428            }
429        };
430        callback.execute();
431    }
432
433    /**
434     * Updates the progress bar.<p>
435     *
436     * @param info the progress info
437     */
438    protected void updateProgressBar(CmsUploadProgessInfo info) {
439
440        switch (info.getState()) {
441            case notStarted:
442                break;
443            case running:
444                m_progressInfo.setProgress(info);
445                stopLoadingAnimation();
446                break;
447            case finished:
448                m_progressInfo.finish();
449                m_mainPanel.displayDialogInfo(Messages.get().key(Messages.GUI_UPLOAD_INFO_FINISHING_0), false);
450                startLoadingAnimation(Messages.get().key(Messages.GUI_UPLOAD_INFO_CREATING_RESOURCES_0), 1500);
451                break;
452            default:
453                break;
454        }
455    }
456
457    /**
458     * Uploads the selected file.<p>
459     */
460    protected void uploadFile() {
461
462        hideOkAndUploadButtons();
463        CmsUploader uploader = new CmsUploader();
464        CmsFileInfo info = m_fileInput.getFiles()[0];
465        info.setOverrideFileName(
466            CmsStringUtil.joinPaths(m_handler.getTargetFolderPath(), m_handler.getFileName(info.getFileName())));
467        uploader.uploadFiles(
468            CmsCoreProvider.get().link(I_CmsUploadConstants.UPLOAD_ACTION_JSP_URI),
469            CmsResource.getFolderPath(m_handler.getTargetFolderPath()),
470            true,
471            null,
472            Collections.singletonList(info),
473            Collections.<String> emptyList(),
474            false,
475            this);
476        showProgress();
477    }
478
479    /**
480     * Closes the dialog after a delay.<p>
481     */
482    private void closeOnSuccess() {
483
484        Timer closeTimer = new Timer() {
485
486            /**
487             * @see com.google.gwt.user.client.Timer#run()
488             */
489            @Override
490            public void run() {
491
492                CmsSingleFileUploadDialog.this.hide();
493            }
494        };
495        closeTimer.schedule(1000);
496    }
497
498    /**
499     * Creates a dialog text button.<p>
500     *
501     * @param buttonText the button text
502     *
503     * @return the button
504     */
505    private CmsPushButton createButton(String buttonText) {
506
507        CmsPushButton button = new CmsPushButton();
508        button.setTitle(buttonText);
509        button.setText(buttonText);
510        button.setSize(I_CmsButton.Size.medium);
511        button.setUseMinWidth(true);
512        return button;
513    }
514
515    /**
516     * Creates the "OK", the "Cancel" and the "Change file" button.<p>
517     */
518    private void createButtons() {
519
520        m_okButton = createButton(org.opencms.gwt.client.Messages.get().key(org.opencms.gwt.client.Messages.GUI_OK_0));
521        m_okButton.addClickHandler(new ClickHandler() {
522
523            /**
524             * @see com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event.dom.client.ClickEvent)
525             */
526            public void onClick(ClickEvent event) {
527
528                uploadFile();
529            }
530        });
531        if (m_fileInput == null) {
532            m_okButton.disable(Messages.get().key(Messages.GUI_REPLACE_NO_FILE_SELECTED_0));
533        }
534        addButton(m_okButton);
535
536        // add a new upload button
537        m_uploadButton = new CmsUploadButton(m_handler);
538        m_uploadButton.addStyleName(I_CmsLayoutBundle.INSTANCE.uploadButton().uploadDialogButton());
539        m_uploadButton.setText(Messages.get().key(Messages.GUI_REPLACE_CHANGE_FILE_0));
540        addButton(m_uploadButton);
541        m_handler.setButton(m_uploadButton);
542    }
543
544    /**
545     * Creates the widget to display the selected file information.<p>
546     *
547     * @param file the file info
548     *
549     * @return the widget
550     */
551    private CmsListItemWidget createFileWidget(CmsFileInfo file) {
552
553        String subTitle;
554        String resourceType = getResourceType(file);
555        if (file.getFileSize() > 0) {
556            subTitle = CmsUploadButton.formatBytes(file.getFileSize()) + " (" + getResourceType(file) + ")";
557        } else {
558            subTitle = resourceType;
559        }
560        CmsListInfoBean infoBean = new CmsListInfoBean(file.getFileName(), subTitle, null);
561        m_fileWidget = new CmsListItemWidget(infoBean);
562        m_fileWidget.setIcon(CmsCoreProvider.get().getResourceTypeIcon(file));
563        return m_fileWidget;
564    }
565
566    /**
567     * Helper method for finishing the upload. 
568     * 
569     * @param jsonObject the JSON received from the server
570     */
571    private void finishUpload(JSONObject jsonObject) {
572
573        JSONValue uploadedFilesVal = jsonObject.get(I_CmsUploadConstants.KEY_UPLOADED_FILE_NAMES);
574        if (uploadedFilesVal != null) {
575            JSONArray uploadedFilesArray = uploadedFilesVal.isArray();
576            if (uploadedFilesArray != null) {
577                List<String> uploadedFiles = new ArrayList<String>();
578                if (uploadedFilesArray != null) {
579                    for (int i = 0; i < uploadedFilesArray.size(); i++) {
580                        JSONString entry = uploadedFilesArray.get(i).isString();
581                        if (entry != null) {
582                            uploadedFiles.add(entry.stringValue());
583                        }
584                    }
585                }
586                if (m_context != null) {
587                    m_context.onUploadFinished(uploadedFiles);
588                }
589            }
590        }
591    }
592
593    /**
594     * Hides the OK and upload button while processing the upload.<p>
595     */
596    private void hideOkAndUploadButtons() {
597
598        m_uploadButton.setVisible(false);
599        m_okButton.setVisible(false);
600    }
601
602    /**
603     * Starts the upload progress bar.<p>
604     */
605    private void showProgress() {
606
607        CmsFileInfo fileInfo = m_fileInput.getFiles()[0];
608        m_progressInfo = new CmsUploadProgressInfo(Collections.singletonList(fileInfo.getFileName()));
609        m_progressInfo.setContentLength(fileInfo.getFileSize());
610        m_mainPanel.setContainerWidget(m_progressInfo);
611        m_updateProgressTimer = new Timer() {
612
613            @Override
614            public void run() {
615
616                updateProgress();
617            }
618        };
619        m_updateProgressTimer.scheduleRepeating(1000);
620
621    }
622
623    /**
624     * Starts the loading animation.<p>
625     *
626     * Used while client is loading files from hard disk into memory.<p>
627     *
628     * @param msg the message that should be displayed below the loading animation (can also be HTML as String)
629     * @param delayMillis the delay to start the animation with
630     */
631    private void startLoadingAnimation(final String msg, int delayMillis) {
632
633        m_loadingTimer = new Timer() {
634
635            @Override
636            public void run() {
637
638                m_mainPanel.showLoadingAnimation(msg);
639            }
640        };
641        if (delayMillis > 0) {
642            m_loadingTimer.schedule(delayMillis);
643        } else {
644            m_loadingTimer.run();
645        }
646    }
647
648    /**
649     * Stops the client loading animation.<p>
650     */
651    private void stopLoadingAnimation() {
652
653        if (m_loadingTimer != null) {
654            m_loadingTimer.cancel();
655        }
656        if (m_clientLoading) {
657            m_mainPanel.removeLoadingAnimation();
658            m_clientLoading = false;
659        }
660    }
661}