/**
* <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.course.config.ui.courselayout;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.services.image.ImageService;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.form.flexible.FormItem;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.elements.FileElement;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
import org.olat.core.gui.components.image.ImageComponent;
import org.olat.core.gui.components.link.Link;
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.translator.Translator;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.AssertException;
import org.olat.core.util.ArrayHelper;
import org.olat.core.util.FileUtils;
import org.olat.core.util.Util;
import org.olat.core.util.coordinate.CoordinatorManager;
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.filters.VFSItemSuffixFilter;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.config.CourseConfig;
import org.olat.course.config.CourseConfigEvent;
import org.olat.course.config.CourseConfigEvent.CourseConfigType;
import org.olat.course.config.ui.courselayout.attribs.AbstractLayoutAttribute;
import org.olat.course.config.ui.courselayout.attribs.PreviewLA;
import org.olat.course.config.ui.courselayout.attribs.SpecialAttributeFormItemHandler;
import org.olat.course.config.ui.courselayout.elements.AbstractLayoutElement;
import org.olat.course.run.environment.CourseEnvironment;
/**
* Description:<br>
* Present different templates for course-layouts and let user generate his own.
*
* <P>
* Initial Date: 01.02.2011 <br>
* @author Roman Haag, roman.haag@frentix.com, http://www.frentix.com
*/
public class CourseLayoutGeneratorController extends FormBasicController {
private static final String ELEMENT_ATTRIBUTE_DELIM = "__";
private static final String PREVIEW_IMAGE_NAME = "preview.png";
private SingleSelection styleSel;
private FileElement logoUpl;
private FormLayoutContainer previewImgFlc;
private FormLayoutContainer styleFlc;
private CustomConfigManager customCMgr;
private LinkedHashMap<String, Map<String, FormItem>> guiWrapper;
private Map<String, Map<String, Object>> persistedCustomConfig;
private FormLayoutContainer logoImgFlc;
private FormLink logoDel;
private boolean elWithErrorExists = false;
private final boolean editable;
private final OLATResourceable courseOres;
private CourseConfig courseConfig;
private CourseEnvironment courseEnvironment;
public CourseLayoutGeneratorController(UserRequest ureq, WindowControl wControl, OLATResourceable courseOres, CourseConfig courseConfig,
CourseEnvironment courseEnvironment, boolean editable) {
super(ureq, wControl);
this.editable = editable;
this.courseOres = courseOres;
this.courseConfig = courseConfig;
this.courseEnvironment = courseEnvironment;
customCMgr = (CustomConfigManager) CoreSpringFactory.getBean("courseConfigManager");
// stack the translator to get attribs/elements
Translator pt = Util.createPackageTranslator(AbstractLayoutAttribute.class, ureq.getLocale(), getTranslator());
pt = Util.createPackageTranslator(AbstractLayoutElement.class, ureq.getLocale(), pt);
setTranslator(pt);
persistedCustomConfig = customCMgr.getCustomConfig(courseEnvironment);
initForm(ureq);
}
/**
* @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#initForm(org.olat.core.gui.components.form.flexible.FormItemContainer, org.olat.core.gui.control.Controller, org.olat.core.gui.UserRequest)
*/
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
setFormTitle("tab.layout.title");
ArrayList<String> keys = new ArrayList<String>();
ArrayList<String> vals = new ArrayList<String>();
ArrayList<String> csss = new ArrayList<String>();
String actualCSSSettings = courseConfig.getCssLayoutRef();
// add a default option
keys.add(CourseLayoutHelper.CONFIG_KEY_DEFAULT);
vals.add(translate("course.layout.default"));
csss.add("");
// check for old legacy template, only available if yet one set
if(actualCSSSettings.startsWith("/") && actualCSSSettings.lastIndexOf("/") == 0) {
keys.add(actualCSSSettings);
vals.add(translate("course.layout.legacy", actualCSSSettings));
csss.add("");
}
// add css from hidden coursecss-folder
VFSContainer coursecssCont = (VFSContainer) courseEnvironment.getCourseFolderContainer().resolve(CourseLayoutHelper.COURSEFOLDER_CSS_BASE);
if (coursecssCont != null) {
coursecssCont.setDefaultItemFilter(new VFSItemSuffixFilter(new String[]{"css"}));
List<VFSItem> coursecssStyles = coursecssCont.getItems();
if (coursecssStyles != null) {
for (VFSItem vfsItem : coursecssStyles) {
keys.add(CourseLayoutHelper.COURSEFOLDER_CSS_BASE + "/" + vfsItem.getName());
vals.add(translate("course.layout.legacy", vfsItem.getName()));
csss.add("");
}
}
}
// get the olat-wide templates
List<VFSItem> templates = CourseLayoutHelper.getCourseThemeTemplates();
if (templates != null) {
for (VFSItem vfsItem : templates) {
if (CourseLayoutHelper.isCourseThemeFolderValid((VFSContainer) vfsItem)){
keys.add(CourseLayoutHelper.CONFIG_KEY_TEMPLATE + vfsItem.getName());
String name = translate("course.layout.template", vfsItem.getName());
vals.add(name);
csss.add("");
}
}
}
// get the predefined template for this course if any
VFSItem predefCont = courseEnvironment.getCourseBaseContainer().resolve(CourseLayoutHelper.LAYOUT_COURSE_SUBFOLDER + "/" + CourseLayoutHelper.CONFIG_KEY_PREDEFINED);
if (predefCont != null && CourseLayoutHelper.isCourseThemeFolderValid((VFSContainer) predefCont)) {
keys.add(CourseLayoutHelper.CONFIG_KEY_PREDEFINED);
vals.add(translate("course.layout.predefined"));
csss.add("");
}
// add option for customizing
keys.add(CourseLayoutHelper.CONFIG_KEY_CUSTOM);
vals.add(translate("course.layout.custom"));
csss.add("");
String[] theKeys = ArrayHelper.toArray(keys);
String[] theValues = ArrayHelper.toArray(vals);
String[] theCssClasses = ArrayHelper.toArray(csss);
styleSel = uifactory.addDropdownSingleselect("course.layout.selector", formLayout, theKeys, theValues, theCssClasses);
styleSel.addActionListener(FormEvent.ONCHANGE);
styleSel.setEnabled(editable);
if (keys.contains(actualCSSSettings)){
styleSel.select(actualCSSSettings, true);
} else {
styleSel.select(CourseLayoutHelper.CONFIG_KEY_DEFAULT, true);
}
previewImgFlc = FormLayoutContainer.createCustomFormLayout("preview.image", getTranslator(), velocity_root + "/image.html");
formLayout.add(previewImgFlc);
previewImgFlc.setLabel("preview.image.label", null);
refreshPreviewImage(ureq, actualCSSSettings);
logoImgFlc = FormLayoutContainer.createCustomFormLayout("logo.image", getTranslator(), velocity_root + "/image.html");
formLayout.add(logoImgFlc);
logoImgFlc.setLabel("logo.image.label", null);
refreshLogoImage(ureq);
// offer upload for 2nd logo
if(editable) {
logoUpl = uifactory.addFileElement(getWindowControl(), "upload.second.logo", formLayout);
logoUpl.addActionListener(FormEvent.ONCHANGE);
Set<String> mimeTypes = new HashSet<String>();
mimeTypes.add("image/*");
logoUpl.limitToMimeType(mimeTypes, "logo.file.type.error", null);
logoUpl.setMaxUploadSizeKB(2048, "logo.size.error", null);
}
// prepare the custom layouter
styleFlc = FormLayoutContainer.createCustomFormLayout("style", getTranslator(), velocity_root + "/style.html");
formLayout.add(styleFlc);
styleFlc.setLabel(null, null);
enableDisableCustom(CourseLayoutHelper.CONFIG_KEY_CUSTOM.equals(actualCSSSettings));
if(editable) {
uifactory.addFormSubmitButton("course.layout.save", formLayout);
}
}
/**
* @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#formInnerEvent(org.olat.core.gui.UserRequest, org.olat.core.gui.components.form.flexible.FormItem, org.olat.core.gui.components.form.flexible.impl.FormEvent)
*/
@Override
protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
if (source == styleSel) {
String selection = styleSel.getSelectedKey();
if (CourseLayoutHelper.CONFIG_KEY_CUSTOM.equals(selection)) {
enableDisableCustom(true);
} else {
enableDisableCustom(false);
}
refreshPreviewImage(ureq, selection); // in any case!
} else if (source == logoUpl && event.wasTriggerdBy(FormEvent.ONCHANGE)) {
if (logoUpl.isUploadSuccess()) {
File newFile = logoUpl.getUploadFile();
String newFilename = logoUpl.getUploadFileName();
boolean isValidFileType = newFilename.toLowerCase().matches(".*[.](png|jpg|jpeg|gif)");
if (!isValidFileType) {
logoUpl.setErrorKey("logo.file.type.error", null);
} else {
logoUpl.clearError();
}
if (processUploadedImage(newFile)){
logoUpl.reset();
showInfo("logo.upload.success");
refreshLogoImage(ureq);
} else {
showError("logo.upload.error");
}
}
} else if (source.getName().contains(ELEMENT_ATTRIBUTE_DELIM)){
// some selections changed, refresh to get new preview
prepareStyleEditor(compileCustomConfigFromGuiWrapper());
} else if (source == logoDel){
VFSItem logo = (VFSItem) logoDel.getUserObject();
logo.delete();
refreshLogoImage(ureq);
}
}
private void enableDisableCustom(boolean onOff){
if (onOff) prepareStyleEditor(persistedCustomConfig);
styleFlc.setVisible(onOff);
styleFlc.setEnabled(editable);
if(logoUpl != null) logoUpl.setVisible(onOff);
logoImgFlc.setVisible(onOff);
}
// process uploaded file according to image size and persist in <course>/layout/logo.xy
private boolean processUploadedImage(File image){
int height = 0;
int width = 0;
int[] size = customCMgr.getImageSize(image);
if (size != null) {
width = size[0];
height = size[1];
} else {
return false;
}
// target file:
String fileType = logoUpl.getUploadFileName().substring(logoUpl.getUploadFileName().lastIndexOf("."));
VFSContainer base = (VFSContainer) courseEnvironment.getCourseBaseContainer().resolve(CourseLayoutHelper.LAYOUT_COURSE_SUBFOLDER);
if (base == null) {
base = courseEnvironment.getCourseBaseContainer().createChildContainer(CourseLayoutHelper.LAYOUT_COURSE_SUBFOLDER);
}
VFSContainer customBase = (VFSContainer) base.resolve("/" + CourseLayoutHelper.CONFIG_KEY_CUSTOM);
if (customBase==null) {
customBase = base.createChildContainer(CourseLayoutHelper.CONFIG_KEY_CUSTOM);
}
if (customBase.resolve("logo" + fileType) != null) customBase.resolve("logo" + fileType).delete();
VFSLeaf targetFile = customBase.createChildLeaf("logo" + fileType);
int maxHeight = CourseLayoutHelper.getLogoMaxHeight();
int maxWidth = CourseLayoutHelper.getLogoMaxWidth();
if (height > maxHeight || width > maxWidth){
// scale image
try {
ImageService helper = CourseLayoutHelper.getImageHelperToUse();
String extension = FileUtils.getFileSuffix(logoUpl.getUploadFileName());
helper.scaleImage(image, extension, targetFile, maxWidth, maxHeight);
} catch (Exception e) {
logError("could not find to be scaled image", e);
return false;
}
} else {
// only persist without scaling
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(image);
out = targetFile.getOutputStream(false);
FileUtils.copy(in, out);
} catch (FileNotFoundException e) {
logError("Problem reading uploaded image to copy", e);
return false;
} finally {
FileUtils.closeSafely(in);
FileUtils.closeSafely(out);
}
}
return true;
}
private void refreshPreviewImage(UserRequest ureq, String template) {
VFSContainer baseFolder = CourseLayoutHelper.getThemeBaseFolder(courseEnvironment, template);
if (baseFolder != null) {
VFSItem preview = baseFolder.resolve("/" + PREVIEW_IMAGE_NAME);
if (preview instanceof VFSLeaf) {
ImageComponent image = new ImageComponent(ureq.getUserSession(), "preview");
previewImgFlc.setVisible(true);
previewImgFlc.put("preview", image);
image.setMedia((VFSLeaf) preview);
image.setMaxWithAndHeightToFitWithin(300, 300);
return;
}
}
previewImgFlc.setVisible(false);
previewImgFlc.remove(previewImgFlc.getComponent("preview"));
}
private void refreshLogoImage(UserRequest ureq){
VFSContainer baseFolder = CourseLayoutHelper.getThemeBaseFolder(courseEnvironment, CourseLayoutHelper.CONFIG_KEY_CUSTOM);
VFSItem logo = customCMgr.getLogoItem(baseFolder);
if (logo instanceof VFSLeaf) {
ImageComponent image = new ImageComponent(ureq.getUserSession(), "preview");
logoImgFlc.setVisible(true);
logoImgFlc.put("preview", image);
image.setMedia((VFSLeaf)logo);
image.setMaxWithAndHeightToFitWithin(300, 300);
logoDel = uifactory.addFormLink("logo.delete", logoImgFlc, Link.BUTTON_XSMALL);
logoDel.setUserObject(logo);
logoDel.setVisible(editable);
return;
}
logoImgFlc.setVisible(false);
logoImgFlc.remove(logoImgFlc.getComponent("preview"));
}
/**
* @see org.olat.core.gui.components.form.flexible.impl.FormBasicController#formOK(org.olat.core.gui.UserRequest)
*/
@Override
protected void formOK(UserRequest ureq) {
String selection = styleSel.getSelectedKey();
ICourse course = CourseFactory.openCourseEditSession(courseOres.getResourceableId());
courseEnvironment = course.getCourseEnvironment();
courseConfig = courseEnvironment.getCourseConfig();
courseConfig.setCssLayoutRef(selection);
if(CourseLayoutHelper.CONFIG_KEY_CUSTOM.equals(selection)){
Map<String, Map<String, Object>> customConfig = compileCustomConfigFromGuiWrapper();
customCMgr.saveCustomConfigAndCompileCSS(customConfig, courseEnvironment);
persistedCustomConfig = customConfig;
if (!elWithErrorExists) prepareStyleEditor(customConfig);
}
CourseFactory.setCourseConfig(course.getResourceableId(), courseConfig);
CourseFactory.closeCourseEditSession(course.getResourceableId(), true);
CoordinatorManager.getInstance().getCoordinator().getEventBus()
.fireEventToListenersOf(new CourseConfigEvent(CourseConfigType.layout, course.getResourceableId()), course);
// inform course-settings-dialog about changes:
fireEvent(ureq, Event.CHANGED_EVENT);
}
private Map<String, Map<String, Object>> compileCustomConfigFromGuiWrapper(){
// get config from wrapper-object
elWithErrorExists = false;
Map<String, Map<String, Object>> customConfig = new HashMap<String, Map<String, Object>>();
for (Iterator<Entry<String, Map<String, FormItem>>> iterator = guiWrapper.entrySet().iterator(); iterator.hasNext();) {
Entry<String, Map<String, FormItem>> type = iterator.next();
String cIdent = type.getKey();
Map<String, Object> elementConfig = new HashMap<String, Object>();
Map<String, FormItem> element = type.getValue();
for (Entry<String, FormItem> entry : element.entrySet()) {
String attribName = entry.getKey();
if (!attribName.equals(PreviewLA.IDENTIFIER)){ // exclude preview
FormItem foItem = entry.getValue();
String value = "";
if (foItem instanceof SingleSelection) {
value = ((SingleSelection)foItem).isOneSelected() ? ((SingleSelection)foItem).getSelectedKey() : "";
} else if (foItem.getUserObject() != null && foItem.getUserObject() instanceof SpecialAttributeFormItemHandler) {
// enclosed item
SpecialAttributeFormItemHandler specHandler = (SpecialAttributeFormItemHandler) foItem.getUserObject();
value = specHandler.getValue();
if (specHandler.hasError()) {
elWithErrorExists = true;
}
} else {
throw new AssertException("implement a getValue for this FormItem to get back a processable value.");
}
elementConfig.put(attribName, value);
}
}
customConfig.put(cIdent, elementConfig);
}
return customConfig;
}
private void prepareStyleEditor(Map<String, Map<String, Object>> customConfig){
guiWrapper = new LinkedHashMap<String, Map<String, FormItem>>(); //keep config order
List<AbstractLayoutElement> allElements = customCMgr.getAllAvailableElements();
List<AbstractLayoutAttribute> allAttribs = customCMgr.getAllAvailableAttributes();
styleFlc.contextPut("allAttribs", allAttribs);
styleFlc.setUserObject(this); // needed reference to get listener back.
for (AbstractLayoutElement abstractLayoutElement : allElements) {
String elementType = abstractLayoutElement.getLayoutElementTypeName();
Map<String, Object> elConf = customConfig.get(elementType);
AbstractLayoutElement concreteElmt = abstractLayoutElement.createInstance(elConf);
HashMap<String, FormItem> elAttribGui = new HashMap<String, FormItem>();
List<AbstractLayoutAttribute> attributes = concreteElmt.getAvailableAttributes();
for (AbstractLayoutAttribute attrib : attributes) {
String compName = elementType + ELEMENT_ATTRIBUTE_DELIM + attrib.getLayoutAttributeTypeName();
FormItem fi = attrib.getFormItem(compName, styleFlc);
fi.addActionListener(FormEvent.ONCHANGE);
elAttribGui.put(attrib.getLayoutAttributeTypeName(), fi);
}
guiWrapper.put(elementType, elAttribGui);
}
styleFlc.contextPut("guiWrapper", guiWrapper);
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose()
*/
@Override
protected void doDispose() {
// nothing to dispose
}
}