/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.commons.controllers.filechooser;
import org.olat.core.commons.controllers.linkchooser.CustomLinkTreeModel;
import org.olat.core.commons.editor.htmleditor.HTMLEditorController;
import org.olat.core.commons.editor.htmleditor.WysiwygFactory;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsPreviewController;
import org.olat.core.commons.modules.bc.FileUploadController;
import org.olat.core.commons.modules.bc.FolderEvent;
import org.olat.core.commons.modules.singlepage.SinglePageController;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.link.LinkFactory;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.BasicController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableCalloutWindowController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.vfs.Quota;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.filters.SystemItemFilter;
import org.olat.core.util.vfs.filters.VFSItemFilter;
/**
* Description:
* <p>This controller provides a view with three link options:
* to browse for a file, to create a new file or to upload a file.
* </p>
*
* Initial date: 18.12.2014<br>
* @author dfurrer, dirk.furrer@frentix.com, http://www.frentix.com
*
*/
public class LinkFileCombiCalloutController extends BasicController {
private VelocityContainer contentVC;
private Link calloutTriggerLink;
private CloseableCalloutWindowController calloutCtr;
private CustomLinkTreeModel customLinkTreeModel;
private Link editLink;
private CloseableModalController cmc;
private Controller currentModalController;
private LayoutMain3ColsPreviewController previewLayoutCtr;
private Link previewLink;
private FileCombiCalloutWindowController combiWindowController;
private VFSContainer baseContainer;
private VFSLeaf file=null;
private String relFilePath;
private boolean relFilPathIsProposal;
private boolean allowEditorRelativeLinks;
/**
*
* @param ureq
* User request
* @param wControl
* Window control
* @param baseContainer
* container from which files can be selected or where files can
* be stored
* @param relFilePath
* path relative to course folder of already selected file, null
* if no file present yet
* @param relFilPathIsProposal
* if true, relFilePath is just a proposal which can be changed
* by user
* @param allowEditorRelativeLinks
* true: editor can link to all files from baseContainer false:
* editor can link only to files relative to the position of the
* edited file;
* @param customLinkTreeModel
* The custom link tree model or NULL if no link tree model used
* in HTML editor
*/
public LinkFileCombiCalloutController(UserRequest ureq, WindowControl wControl, VFSContainer baseContainer, String relFilePath,
boolean relFilPathIsProposal, boolean allowEditorRelativeLinks, CustomLinkTreeModel customLinkTreeModel) {
super(ureq, wControl);
this.baseContainer = baseContainer;
this.relFilPathIsProposal = relFilPathIsProposal;
this.allowEditorRelativeLinks = allowEditorRelativeLinks;
this.customLinkTreeModel = customLinkTreeModel;
// Main container for everything
contentVC = createVelocityContainer("combiFileCallout");
putInitialPanel(contentVC);
// Preview link
previewLink = LinkFactory.createCustomLink("command.preview", "command.preview", relFilePath, Link.NONTRANSLATED, contentVC, this);
previewLink.setElementCssClass("o_sel_filechooser_preview");
previewLink.setIconLeftCSS("o_icon o_icon-fw o_icon_preview");
previewLink.setCustomEnabledLinkCSS("o_preview");
// Button to edit or create the file
editLink = LinkFactory.createButtonSmall("command.edit", contentVC, this);
editLink.setPrimary(true);
editLink.setIconLeftCSS("o_icon o_icon-fw o_icon_edit");
// Callout button with the three links next to edit button
calloutTriggerLink = LinkFactory.createButtonSmall("calloutTriggerLink", contentVC, this);
// Load file from configuration and update links
setRelFilePath(relFilePath);
}
@Override
public void event(UserRequest ureq, Component source, Event event) {
if(source == editLink){
doOpenWysiwygEditor(ureq);
}
if (source == calloutTriggerLink) {
doOpenCallout(ureq);
}
if (source == previewLink){
doShowPreview(ureq);
}
}
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
if(combiWindowController == source) {
doOpenFileChanger(ureq, event.getCommand());
} else if (source instanceof FileChooserController) {
// catch the events from the file chooser controller here
if (event instanceof FileChoosenEvent) {
FileChoosenEvent fce = (FileChoosenEvent)event;
file = (VFSLeaf)FileChooserUIFactory.getSelectedItem(fce);
relFilPathIsProposal = false;
setRelFilePath(FileChooserUIFactory.getSelectedRelativeItemPath(fce, baseContainer, null));
fireEvent(ureq, Event.DONE_EVENT);
} else if (event == Event.FAILED_EVENT) {
// selection failed for unknown reason
} else if(event == Event.CANCELLED_EVENT){
// nothing to do
}
cleanupModal(true);
} else if (source instanceof FileUploadController) {
if(event == Event.DONE_EVENT){
FileUploadController uploadCtr = (FileUploadController) source;
VFSLeaf newFile = uploadCtr.getUploadedFile();
if (newFile.getName().toLowerCase().endsWith("zip")) {
// Cleanup modal first
cleanupModal(true);
// Unzip file and open file chooser in new modal
VFSContainer zipContainer = doUnzip(newFile, newFile.getParentContainer());
if (zipContainer != null) {
FileChooserController fileChooserCtr = FileChooserUIFactory.createFileChooserController(ureq, getWindowControl(), zipContainer, null, true);
fileChooserCtr.setShowTitle(true);
displayModal(fileChooserCtr);
return;
}
} else {
// All other files
file = uploadCtr.getUploadedFile();
relFilPathIsProposal = false;
setRelFilePath(VFSManager.getRelativeItemPath(file, baseContainer, null));
fireEvent(ureq, Event.DONE_EVENT);
}
} else if (event.getCommand().equals(FolderEvent.UPLOAD_EVENT)) {
// Do nothing, ignore this internal event. When finished, done is fired
return;
} else if(event == Event.CANCELLED_EVENT){
// nothing to do
}
cleanupModal(true);
} else if (source instanceof HTMLEditorController) {
if(event == Event.DONE_EVENT){
relFilPathIsProposal = false;
editLink.setCustomDisplayText(translate("command.edit"));
fireEvent(ureq, Event.DONE_EVENT);
} else if(event == Event.CANCELLED_EVENT) {
// nothing to do
}
cleanupModal(true);
updateLinks();
} else if (source instanceof FileCreatorController) {
if(event == Event.DONE_EVENT){
FileCreatorController createCtr = (FileCreatorController) source;
file = createCtr.getCreatedFile();
relFilPathIsProposal = false;
setRelFilePath(VFSManager.getRelativeItemPath(file, baseContainer, null));
fireEvent(ureq, Event.DONE_EVENT);
cleanupModal(true);
// Now open html editor
doOpenWysiwygEditor(ureq);
} else if (event == Event.CANCELLED_EVENT){
cleanupModal(true);
}
} else if (source == cmc && event == CloseableModalController.CLOSE_MODAL_EVENT) {
// User closed dialog, same as cancel in a sub-controller
cleanupModal(false);
} else if (source == previewLayoutCtr && event == Event.BACK_EVENT) {
removeAsListenerAndDispose(previewLayoutCtr);
}
super.event(ureq, source, event);
}
/////////////////// helper methods to implement event loop
private void doOpenWysiwygEditor(UserRequest ureq) {
if(relFilPathIsProposal){
file = VFSManager.resolveOrCreateLeafFromPath(baseContainer, relFilePath);
}
if (file == null) {
// huh? no idea what happend, do nothing and log error
logError("Could not load or create file with relFilePath::" + relFilePath + " in baseContainer::" + VFSManager.getRealPath(baseContainer), null);
return;
}
// Configure editor depending on limitEditorToRelativeFiles flag
// either based on baseContainer or the files direct parent
VFSContainer editorBaseContainer = baseContainer;
String editorRelPath = relFilePath;
if (!allowEditorRelativeLinks && relFilePath.indexOf("/", 1) != 0) {
editorBaseContainer = file.getParentContainer();
editorRelPath = file.getName();
}
// Open HTML editor in dialog
Controller wysiwygCtr = WysiwygFactory.createWysiwygControllerWithInternalLink(ureq, getWindowControl(), editorBaseContainer, editorRelPath, true, customLinkTreeModel);
displayModal(wysiwygCtr);
}
private void doOpenCallout(UserRequest ureq) {
if (combiWindowController == null) {
// Create file combi and callout controller only once. Later on
// use activate and deactivate to show/hide the callout
combiWindowController = new FileCombiCalloutWindowController(ureq, getWindowControl());
calloutCtr = new CloseableCalloutWindowController(ureq, getWindowControl(), combiWindowController.getInitialComponent(), "o_c"
+ calloutTriggerLink.getDispatchID(), null, true, null);
listenTo(combiWindowController);
listenTo(calloutCtr);
}
calloutCtr.activate();
}
private void doShowPreview(UserRequest ureq) {
SinglePageController previewController = new SinglePageController(ureq, getWindowControl(), file.getParentContainer(), file.getName(), false);
previewLayoutCtr = new LayoutMain3ColsPreviewController(ureq, getWindowControl(), null, previewController.getInitialComponent(), null);
previewLayoutCtr.addDisposableChildController(previewController);
previewLayoutCtr.activate();
listenTo(previewLayoutCtr);
}
public void doOpenFileChanger(UserRequest ureq, String tool) {
// close callout and open appropriate file changer controller
calloutCtr.deactivate();
Controller toolCtr = null;
if(tool.equals("chooseLink")) {
VFSItemFilter filter = new SystemItemFilter();
FileChooserController fileChooserCtr = FileChooserUIFactory.createFileChooserController(ureq, getWindowControl(), baseContainer, filter, true);
fileChooserCtr.setShowTitle(true);
fileChooserCtr.selectPath(relFilePath);
toolCtr = fileChooserCtr;
}
if(tool.equals("createLink")){
String folderPath = null;
if (StringHelper.containsNonWhitespace(relFilePath)) {
// remove file name from relFilePath to represent directory path
folderPath = relFilePath.substring(0, relFilePath.lastIndexOf("/"));
}
toolCtr = new FileCreatorController(ureq, getWindowControl(), baseContainer, folderPath);
}
if(tool.equals("uploadLink")){
long quotaLeftKB = VFSManager.getQuotaLeftKB(baseContainer);
String folderPath = null;
if (StringHelper.containsNonWhitespace(relFilePath)) {
// remove file name from relFilePath to represent directory path
folderPath = relFilePath.substring(0, relFilePath.lastIndexOf("/"));
}
toolCtr = new FileUploadController(getWindowControl(), baseContainer, ureq, quotaLeftKB, quotaLeftKB, null, false, true, false, false, true, true, folderPath);
}
displayModal(toolCtr);
}
private VFSContainer doUnzip(VFSLeaf vfsItem, VFSContainer currentContainer) {
String name = vfsItem.getName();
// we make a new folder with the same name as the zip file
String sZipContainer = name.substring(0, name.length() - 4);
// if zip already exists, create another folder 1,2,3 etc.
VFSContainer zipContainer = currentContainer.createChildContainer(sZipContainer);
int i = 1;
while (zipContainer == null && i<100) {
i++;
sZipContainer = FileUtils.appendNumberAtTheEndOfFilename(sZipContainer, i);
zipContainer = currentContainer.createChildContainer(sZipContainer);
}
if (!ZipUtil.unzip(vfsItem, zipContainer)) {
// operation failed - rollback
zipContainer.delete();
return null;
} else {
// check quota
long quotaLeftKB = VFSManager.getQuotaLeftKB(currentContainer);
if (quotaLeftKB != Quota.UNLIMITED && quotaLeftKB < 0) {
// quota exceeded - rollback
zipContainer.delete();
getWindowControl().setError(translate("QuotaExceeded"));
return null;
}
}
return zipContainer;
}
/**
* Helper to cleanup the current modal and its content controller
*
* @param pop
* true: pop dialog from stack; false: don't pop dialog, already
* done
*/
private void cleanupModal(boolean pop) {
if (pop) {
cmc.deactivate();
}
removeAsListenerAndDispose(currentModalController);
removeAsListenerAndDispose(cmc);
}
/**
* Helper to setup the modal controller for the content and activate it
* @param newModalContentController
*/
private void displayModal(Controller newModalContentController) {
if (newModalContentController == null) return;
currentModalController = newModalContentController;
listenTo(currentModalController);
cmc = new CloseableModalController(getWindowControl(), "close", newModalContentController.getInitialComponent());
listenTo(cmc);
cmc.activate();
}
/**
* Helper to update the link visibility and labels according to the business logic
*/
private void updateLinks(){
// Set preview link active if a file is configured
if(file == null){
if (StringHelper.containsNonWhitespace(relFilePath)) {
previewLink.setCustomDisplayText(relFilePath);
} else {
previewLink.setCustomDisplayText(translate("no.file.chosen"));
}
previewLink.setEnabled(false);
} else {
contentVC.contextPut("deleted", Boolean.valueOf(false));
previewLink.setCustomDisplayText(relFilePath);
previewLink.setEnabled(true);
}
// Enable edit link when file is editable
if(isEditorEnabled()){
if (file == null) {
editLink.setCustomDisplayText(translate("command.create"));
} else {
editLink.setCustomDisplayText(translate("command.edit"));
}
contentVC.put("command.edit", editLink);
} else {
contentVC.remove(editLink);
}
// Set display text on callout depending on available path and file
if(StringHelper.containsNonWhitespace(relFilePath)){
if (file == null) {
calloutTriggerLink.setCustomDisplayText(translate("calloutTrigerLink.select.site"));
calloutTriggerLink.setIconLeftCSS("o_icon o_icon-fw o_icon_replace");
} else {
calloutTriggerLink.setCustomDisplayText(translate("calloutTriggerLink.replace"));
calloutTriggerLink.setIconLeftCSS("o_icon o_icon-fw o_icon_replace");
}
} else {
calloutTriggerLink.setCustomDisplayText(translate("calloutTriggerLink.replace"));
calloutTriggerLink.setIconLeftCSS("o_icon o_icon-fw o_icon_select");
}
}
///// public getter and setter
public void setAllowEditorRelativeLinks(boolean allowEditorRelativeLinks) {
this.allowEditorRelativeLinks = allowEditorRelativeLinks;
}
public void setRelFilePath(String relFilePath) {
this.relFilePath = relFilePath;
if(StringHelper.containsNonWhitespace(relFilePath)) {
VFSItem item = baseContainer.resolve(relFilePath);
if(!(item instanceof VFSContainer)) {
file = (VFSLeaf)item;
if (file == null && !this.relFilPathIsProposal) {
// System assumed that this page would exist. Maybe deleted by
// someone in folder. Tell user and offer to create the page
// again.
this.relFilPathIsProposal = true;
contentVC.contextPut("deleted", Boolean.valueOf(true));
}
}
}
// Update all links in the GUI
updateLinks();
}
public boolean isDoProposal() {
return relFilPathIsProposal;
}
public VFSLeaf getFile(){
return file;
}
public boolean isEditorEnabled() {
// enable html editor for html files
if(StringHelper.containsNonWhitespace(relFilePath)) {
String lowercase = relFilePath.toLowerCase().trim();
if (lowercase.endsWith(".html") || lowercase.endsWith(".htm")) {
return true;
}
}
// disable html editor for all other cases
return false;
}
@Override
protected void doDispose() {
// controllers auto-disposed by basic controller
}
}