diff --git a/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java b/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java
index 318ac1de2c657d6b7eb9ef285dda5d6b53bbabc2..4d0d1f751b81e8002c2b0da6eb6250a48fc69f77 100644
--- a/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java
+++ b/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java
@@ -38,6 +38,7 @@ import org.olat.course.condition.interpreter.score.GetPassedFunction;
 import org.olat.course.condition.interpreter.score.GetPassedWithCourseIdFunction;
 import org.olat.course.condition.interpreter.score.GetScoreFunction;
 import org.olat.course.condition.interpreter.score.GetScoreWithCourseIdFunction;
+import org.olat.course.db.interpreter.GetUserCourseDBFunction;
 import org.olat.course.editor.CourseEditorEnv;
 import org.olat.course.run.userview.UserCourseEnvironment;
 
@@ -121,6 +122,7 @@ public class ConditionInterpreter {
 		eaf = new EvalAttributeFunction(userCourseEnv, EvalAttributeFunction.FUNCTION_TYPE_ATTRIBUTE_STARTS_WITH);
 		env.addFunction(eaf.name, eaf);
 		env.addFunction(GetUserPropertyFunction.name, new GetUserPropertyFunction(userCourseEnv));
+		env.addFunction(GetUserCourseDBFunction.name, new GetUserCourseDBFunction(userCourseEnv));
 		env.addFunction(HasLanguageFunction.name, new HasLanguageFunction(userCourseEnv));
 		env.addFunction(InInstitutionFunction.name, new InInstitutionFunction(userCourseEnv));
 		env.addFunction(IsCourseCoachFunction.name, new IsCourseCoachFunction(userCourseEnv));
diff --git a/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java b/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java
index 4883a4bea7ec310078967787e9a4582e826a2dff..0c9f6152cbce30482527eea85fe1a8b7dcd8c135 100644
--- a/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java
+++ b/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java
@@ -30,6 +30,7 @@ import org.olat.course.condition.interpreter.score.GetPassedFunction;
 import org.olat.course.condition.interpreter.score.GetPassedWithCourseIdFunction;
 import org.olat.course.condition.interpreter.score.GetScoreFunction;
 import org.olat.course.condition.interpreter.score.GetScoreWithCourseIdFunction;
+import org.olat.course.db.interpreter.GetUserCourseDBFunction;
 import org.olat.course.editor.CourseEditorEnv;
 import org.olat.course.run.userview.UserCourseEnvironment;
 
@@ -80,6 +81,7 @@ public class OnlyGroupConditionInterpreter extends ConditionInterpreter{
 		env.addFunction("hasAttribute", new DummyBooleanFunction(userCourseEnv));
 		env.addFunction("isInAttribute", new DummyBooleanFunction(userCourseEnv));
 		env.addFunction(GetUserPropertyFunction.name, new DummyStringFunction(userCourseEnv));
+		env.addFunction(GetUserCourseDBFunction.name, new DummyStringFunction(userCourseEnv));
 		env.addFunction(HasLanguageFunction.name, new DummyBooleanFunction(userCourseEnv));
 		env.addFunction(InInstitutionFunction.name, new DummyBooleanFunction(userCourseEnv));
 		env.addFunction(IsCourseCoachFunction.name, new DummyBooleanFunction(userCourseEnv));
diff --git a/src/main/java/org/olat/course/db/CourseDBEntry.java b/src/main/java/org/olat/course/db/CourseDBEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..6515b47c11467c386d2f69fb8d55fb323efde8c3
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CourseDBEntry.java
@@ -0,0 +1,47 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db;
+
+import org.olat.core.id.Identity;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CourseDBEntry
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public interface CourseDBEntry {
+	
+	public Long getCourseKey();
+	
+	public Identity getIdentity();
+	
+	public String getCategory();
+	
+	public String getName();
+	
+	public Object getValue();
+	
+	public void setValue(Object value);
+}
diff --git a/src/main/java/org/olat/course/db/CourseDBManager.java b/src/main/java/org/olat/course/db/CourseDBManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb20ea91cb9aa3644f315d90e6d942c3e67bd207
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CourseDBManager.java
@@ -0,0 +1,59 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db;
+
+import java.util.List;
+
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.configuration.AbstractOLATModule;
+import org.olat.core.configuration.ConfigOnOff;
+import org.olat.core.id.Identity;
+import org.olat.course.ICourse;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CourseDBManager
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public abstract class CourseDBManager extends AbstractOLATModule implements ConfigOnOff {
+
+	public static CourseDBManager getInstance() {
+		return (CourseDBManager)CoreSpringFactory.getBean("courseDBManager");
+	}
+	
+	public abstract boolean isEnabled();
+	
+	public abstract void reset(ICourse course, String category);
+	
+	public abstract CourseDBEntry getValue(ICourse course, Identity identity, String category, String name);
+	
+	public abstract CourseDBEntry getValue(Long courseResourceId, Identity identity, String category, String name);
+	
+	public abstract boolean deleteValue(ICourse course, Identity identity, String category, String name);
+	
+	public abstract CourseDBEntry setValue(ICourse course, Identity identity, String category, String name, Object value);
+	
+	public abstract List<CourseDBEntry> getValues(ICourse course, Identity identity, String category, String name);
+}
diff --git a/src/main/java/org/olat/course/db/CourseDBMediaResource.java b/src/main/java/org/olat/course/db/CourseDBMediaResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..709b575e518972fba069658d5ccb15242212d31e
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CourseDBMediaResource.java
@@ -0,0 +1,63 @@
+package org.olat.course.db;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.util.StringHelper;
+
+/**
+ * 
+ * Description:<br>
+ * 
+ * <P>
+ * Initial Date:  13 déc. 2011 <br>
+ *
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class CourseDBMediaResource implements MediaResource {
+	
+	private final String charset;
+	private final String fileName;
+	private final byte[] content;
+	
+	public CourseDBMediaResource(String charset, String fileName, byte[] content) {
+		this.charset = charset;
+		this.fileName = fileName;
+		this.content = content;
+	}
+
+	@Override
+	public String getContentType() {
+		return "application/vnd.ms-excel; charset=" + charset;
+	}
+
+	@Override
+	public Long getSize() {
+		return content == null ? new Long(0) : content.length;
+	}
+
+	@Override
+	public InputStream getInputStream() {
+		return new ByteArrayInputStream(content);
+	}
+
+	@Override
+	public Long getLastModified() {
+		return -1l;
+	}
+
+	@Override
+	public void prepare(HttpServletResponse hres) {
+		hres.setHeader("Content-Disposition","filename=\"" + StringHelper.urlEncodeISO88591(fileName) + "\"");
+		hres.setHeader("Content-Description",StringHelper.urlEncodeISO88591(fileName));
+
+	}
+
+	@Override
+	public void release() {
+		//
+	}
+}
diff --git a/src/main/java/org/olat/course/db/CustomDBAddController.java b/src/main/java/org/olat/course/db/CustomDBAddController.java
new file mode 100644
index 0000000000000000000000000000000000000000..acac3fb88c3497916a62755d18269613630f1499
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CustomDBAddController.java
@@ -0,0 +1,81 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CustomDBAddController
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public class CustomDBAddController extends FormBasicController {
+
+	private TextElement categoryEl;
+	
+	public CustomDBAddController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		categoryEl = uifactory.addTextElement("category", "customDb.category", 32, "", formLayout);
+		
+		final FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttonLayout", getTranslator());
+		formLayout.add(buttonLayout);
+		uifactory.addFormSubmitButton("ok", buttonLayout);
+		uifactory.addFormCancelButton("cancel", buttonLayout, ureq, getWindowControl());
+	}
+	
+	public String getCategory() {
+		return categoryEl.getValue();
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+	
+	
+}
diff --git a/src/main/java/org/olat/course/db/CustomDBController.java b/src/main/java/org/olat/course/db/CustomDBController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec457032e0aa5845dd5714aa6a70d3a4325bf6b6
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CustomDBController.java
@@ -0,0 +1,278 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+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.FormLink;
+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.link.Link;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.helpers.Settings;
+import org.olat.core.id.User;
+import org.olat.core.id.UserConstants;
+import org.olat.core.util.ExportUtil;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.coordinate.CoordinatorManager;
+import org.olat.core.util.coordinate.SyncerExecutor;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.properties.CoursePropertyManager;
+import org.olat.course.tree.CourseEditorTreeNode;
+import org.olat.properties.Property;
+import org.olat.restapi.security.RestSecurityHelper;
+import org.olat.user.UserManager;
+
+/**
+ * 
+ * Description:<br>
+ * Controller to manage a custom DB, export, delete...
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class CustomDBController extends FormBasicController {
+
+	private List<FormLink> resetDbs = new ArrayList<FormLink>();
+	private List<FormLink> deleteDbs = new ArrayList<FormLink>();
+	private List<FormLink> exportDbs = new ArrayList<FormLink>();
+	private FormLayoutContainer dbListLayout;
+	
+	private final Long courseKey;
+	
+	public CustomDBController(UserRequest ureq, WindowControl wControl, Long courseKey) {
+		super(ureq, wControl, LAYOUT_VERTICAL);
+		this.courseKey = courseKey;
+		initForm(ureq);
+	}
+	
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		setFormTitle("customDb.custom_db");
+		
+		dbListLayout = FormLayoutContainer.createDefaultFormLayout("dbListLayout", getTranslator());
+		formLayout.add(dbListLayout);
+		updateDBList(dbListLayout);
+	}
+	
+	public void updateUI() {
+		FormItem[] items = new FormItem[dbListLayout.getFormComponents().size()];
+		items = dbListLayout.getFormComponents().values().toArray(items);
+		for(FormItem item:items) {
+			dbListLayout.remove(item);
+		}
+		initialPanel.setDirty(true);
+		updateDBList(dbListLayout);
+	}
+	
+	private void updateDBList(FormItemContainer formLayout) {
+		ICourse course = CourseFactory.loadCourse(courseKey);
+		CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
+		CourseNode rootNode = ((CourseEditorTreeNode)course.getEditorTreeModel().getRootNode()).getCourseNode();
+		Property p = cpm.findCourseNodeProperty(rootNode, null, null, CustomDBMainController.CUSTOM_DB);
+		if(p != null && p.getTextValue() != null) {
+			String[] dbs = p.getTextValue().split(":");
+			
+			int count = 0;
+			for(String db:dbs) {
+				if(!StringHelper.containsNonWhitespace(db)) continue;
+				
+				uifactory.addStaticExampleText("category_" + count, "customDb.category", db, formLayout);
+				String url = Settings.getServerContextPathURI() + RestSecurityHelper.SUB_CONTEXT + "/repo/courses/" + courseKey + "/db/" + db;
+				uifactory.addStaticExampleText("url_" + count, "customDb.url", url, formLayout);
+				
+				final FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttonLayout_" + count, getTranslator());
+				formLayout.add(buttonLayout);
+				
+				FormLink resetDb = uifactory.addFormLink("db-reset_" + count, "customDb.reset", "customDb.reset", buttonLayout, Link.BUTTON_SMALL);
+				resetDb.setUserObject(db);
+				FormLink deleteDb = uifactory.addFormLink("db-delete_" + count, "delete", "delete", buttonLayout, Link.BUTTON_SMALL);
+				deleteDb.setUserObject(db);
+				FormLink exportDb = uifactory.addFormLink("db-export_" + count, "customDb.export", "customDb.export", buttonLayout, Link.BUTTON_SMALL);
+				exportDb.setUserObject(db);
+				
+				resetDbs.add(resetDb);
+				deleteDbs.add(deleteDb);
+				exportDbs.add(exportDb);
+				
+				count++;
+			}
+		}
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		//do nothing as default
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if (resetDbs.contains(source)) {
+			resetDb((String)source.getUserObject());
+		} else if (deleteDbs.contains(source)) {
+			deleteDb((String)source.getUserObject());
+		} else if (exportDbs.contains(source)) {
+			exportDb(ureq, (String)source.getUserObject());
+		}
+	}
+	
+	private void resetDb(String category) {
+		CourseDBManager dbManager = CourseDBManager.getInstance();
+		ICourse course = CourseFactory.loadCourse(courseKey);
+		dbManager.reset(course, category);
+	}
+	
+	private void deleteDb(String category) {
+		CourseDBManager dbManager = CourseDBManager.getInstance();
+		ICourse course = CourseFactory.loadCourse(courseKey);
+		dbManager.reset(course, category);
+		deleteCustomDb(course, category);
+		updateUI();
+	}
+	
+	private void exportDb(UserRequest ureq, String category) {
+		ICourse course = CourseFactory.loadCourse(courseKey);
+    byte[] content = getDbsContent(course.getCourseTitle(), category);
+    if(content == null) {
+    	showError("customDb.export.failed");
+    } else {
+    	UserManager um = UserManager.getInstance();
+      String charset = um.getUserCharset(ureq.getIdentity());
+    	String fileName = ExportUtil.createFileNameWithTimeStamp("DBS_" + course.getCourseTitle(), "xls");
+			MediaResource export = new CourseDBMediaResource(charset, fileName, content);
+			ureq.getDispatchResult().setResultingMediaResource(export);
+    }
+	}
+	
+	private byte[] getDbsContent(String courseTitle, String category) {
+		ICourse course = CourseFactory.loadCourse(courseKey);
+		List<CourseDBEntry> content = CourseDBManager.getInstance().getValues(course, null, category, null);
+
+		Workbook wb = new HSSFWorkbook();
+		CellStyle headerCellStyle = getHeaderCellStyle(wb);
+		Sheet exportSheet = wb.createSheet(courseTitle);
+		
+		//create the headers
+		Row headerRow = exportSheet.createRow(0);
+		Cell cell = headerRow.createCell(0);
+		cell.setCellValue(translate("customDb.category"));
+		cell.setCellStyle(headerCellStyle);
+		
+		cell = headerRow.createCell(1);
+		cell.setCellValue(translate("customDb.entry.identity"));
+		cell.setCellStyle(headerCellStyle);
+		
+		cell = headerRow.createCell(2);
+		cell.setCellValue(translate("customDb.entry.name"));
+		cell.setCellStyle(headerCellStyle);
+		
+		cell = headerRow.createCell(3);
+		cell.setCellValue(translate("customDb.entry.value"));
+		cell.setCellStyle(headerCellStyle);
+
+		int count = 0;
+		for (CourseDBEntry entry:content) {
+			User user = entry.getIdentity().getUser();
+			String name = user.getProperty(UserConstants.FIRSTNAME, null) + " " + user.getProperty(UserConstants.LASTNAME, null); 
+
+			Row dataRow = exportSheet.createRow(++count);
+			Cell dataCell = dataRow.createCell(0);
+			dataCell.setCellValue(entry.getCategory());
+			
+			dataCell = dataRow.createCell(1);
+			dataCell.setCellValue(name);
+
+			if(StringHelper.containsNonWhitespace(entry.getName())) {
+				dataCell = dataRow.createCell(2);
+				dataCell.setCellValue(entry.getName());
+			}
+			
+			if(entry.getValue() != null) {
+				dataCell = dataRow.createCell(3);
+				dataCell.setCellValue(entry.getValue().toString());
+			}
+		}
+		
+		try {
+			ByteArrayOutputStream fos = new ByteArrayOutputStream();
+			wb.write(fos);
+			fos.flush();
+			return fos.toByteArray();
+		} catch (IOException e) {
+			logError("", e);
+		}
+		return null;
+	}
+
+	private CellStyle getHeaderCellStyle(final Workbook wb) {
+		CellStyle cellStyle = wb.createCellStyle();
+		Font boldFont = wb.createFont();
+		boldFont.setBoldweight(Font.BOLDWEIGHT_BOLD);
+		cellStyle.setFont(boldFont);
+		return cellStyle;
+	}
+	
+	private void deleteCustomDb(final ICourse course, final String category) {
+		CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(course, new SyncerExecutor() {
+			@Override
+			public void execute() {
+				CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
+				CourseNode rootNode = ((CourseEditorTreeNode)course.getEditorTreeModel().getRootNode()).getCourseNode();
+				Property p = cpm.findCourseNodeProperty(rootNode, null, null, CustomDBMainController.CUSTOM_DB);
+				if(p != null && p.getTextValue() != null) {
+					String[] dbs = p.getTextValue().split(":");
+					StringBuilder currentDbs = new StringBuilder();
+					for(String db:dbs) {
+						if(!db.equals(category)) {
+							currentDbs.append(db).append(':');
+						}
+					}
+					p.setTextValue(currentDbs.toString());
+					cpm.updateProperty(p);
+				}
+			}
+		});
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/db/CustomDBMainController.java b/src/main/java/org/olat/course/db/CustomDBMainController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0329ffed93a6f4e52c7077a90a035912cb76aca3
--- /dev/null
+++ b/src/main/java/org/olat/course/db/CustomDBMainController.java
@@ -0,0 +1,150 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.tree.GenericTreeNode;
+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.generic.closablewrapper.CloseableModalController;
+import org.olat.core.gui.control.generic.layout.GenericMainController;
+import org.olat.core.gui.control.generic.tool.ToolController;
+import org.olat.core.gui.control.generic.tool.ToolFactory;
+import org.olat.core.util.coordinate.CoordinatorManager;
+import org.olat.core.util.coordinate.SyncerExecutor;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.properties.CoursePropertyManager;
+import org.olat.course.tree.CourseEditorTreeNode;
+import org.olat.properties.Property;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CustomDBMainController
+ * 
+ * <P>
+ * Initial Date:  7 avr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public class CustomDBMainController extends GenericMainController {
+	
+	public static final String CUSTOM_DB = "custom_db";
+	
+	private ICourse course;
+	private ToolController toolC;
+	
+	private CustomDBController dbController;
+	private CustomDBAddController addController;
+	private CloseableModalController cmc;
+
+	public CustomDBMainController(UserRequest ureq, WindowControl windowControl, ICourse course) {
+		super(ureq, windowControl);
+		this.course = course;
+	
+		// Tool and action box
+		toolC = ToolFactory.createToolController(getWindowControl());
+		listenTo(toolC);
+		toolC.addHeader(translate("tool.name"));
+		toolC.addLink("cmd.new_db", translate("command.new_db"), null, "b_new");
+		toolC.addLink("cmd.close", translate("command.closedb"), null, "b_toolbox_close");
+		setToolController(toolC);
+		
+		//set main node
+		GenericTreeNode root = new GenericTreeNode();
+		root.setTitle(translate("main.menu.title"));
+		root.setAltText(translate("main.menu.title.alt"));
+		root.setUserObject("dbs");
+		addChildNodeToPrepend(root);
+
+		init(ureq);
+	}
+
+	@Override
+	protected void doDispose() {
+		// controllers disposed by BasicController:
+	}
+	
+	private void disposeAddController() {
+		removeAsListenerAndDispose(addController);
+		removeAsListenerAndDispose(cmc);
+		addController = null;
+		cmc = null;
+	}
+
+	/**
+	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
+	 *      org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
+	 */
+	public void event(UserRequest ureq, Controller source, Event event) {
+		if (source == toolC) {
+			if (event.getCommand().equals("cmd.close")) {
+				doDispose();
+				fireEvent(ureq, Event.DONE_EVENT);
+			} else if (event.getCommand().equals("cmd.new_db")) {
+				removeAsListenerAndDispose(addController);
+				addController = new CustomDBAddController(ureq, getWindowControl());
+				listenTo(addController);
+				removeAsListenerAndDispose(cmc);
+				cmc = new CloseableModalController(getWindowControl(), translate("close"), addController.getInitialComponent());
+				listenTo(cmc);
+				cmc.activate();
+			}
+		} else if (source == addController) {
+			if(event == Event.DONE_EVENT) {
+				String category = addController.getCategory();
+				addCustomDb(category);
+				dbController.updateUI();
+			}
+			cmc.deactivate();
+			disposeAddController();
+		}
+	}
+	
+	@Override
+	protected Controller handleOwnMenuTreeEvent(Object uobject, UserRequest ureq) {
+		if("dbs".equals(uobject)) {
+			dbController = new CustomDBController(ureq, getWindowControl(), course.getResourceableId());
+			return dbController;
+		}
+		return null;
+	}
+	
+	private void addCustomDb(final String category) {
+		CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(course, new SyncerExecutor() {
+			@Override
+			public void execute() {
+				CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
+				CourseNode rootNode = ((CourseEditorTreeNode)course.getEditorTreeModel().getRootNode()).getCourseNode();
+				Property p = cpm.findCourseNodeProperty(rootNode, null, null, CUSTOM_DB);
+				if(p == null) {
+					p = cpm.createCourseNodePropertyInstance(rootNode, null, null, CUSTOM_DB, null, null, null, category);
+					cpm.saveProperty(p);
+				} else {
+					String currentDbs = p.getTextValue();
+					p.setTextValue(currentDbs + ":" + category);
+					cpm.updateProperty(p);
+				}
+			}
+		});
+	}
+}
diff --git a/src/main/java/org/olat/course/db/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/db/_i18n/LocalStrings_de.properties
new file mode 100644
index 0000000000000000000000000000000000000000..52c6721b44fd91f83cbc1e0f46152d0683a63ad1
--- /dev/null
+++ b/src/main/java/org/olat/course/db/_i18n/LocalStrings_de.properties
@@ -0,0 +1,16 @@
+#Mon Mar 02 09:54:04 CET 2009
+command.new_db=Neue Datenbank erstellen
+command.closedb=Schliessen
+customDb.custom_db=Datenbank
+customDb.create=Erstellen
+customDb.reset=Zurücksetzen
+customDb.export=Exportieren
+customDb.category=Name
+customDb.entry.identity=Benutzer
+customDb.entry.name=Name
+customDb.entry.value=Wert
+customDb.url=URL
+main.menu.title=Datenbanken
+main.menu.title.alt=Kurs Datenbanken
+tool.name=Datenbanken
+customDb.export.failed=
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/db/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/db/_i18n/LocalStrings_en.properties
new file mode 100644
index 0000000000000000000000000000000000000000..aacbccf15b98cdcaea30c130a4d719adc6876a78
--- /dev/null
+++ b/src/main/java/org/olat/course/db/_i18n/LocalStrings_en.properties
@@ -0,0 +1,15 @@
+#Thu May 26 09:44:01 CEST 2011
+command.closedb=Close
+command.new_db=Create new database
+customDb.category=Name
+customDb.create=Create
+customDb.custom_db=Database
+customDb.entry.identity=User
+customDb.entry.name=Name
+customDb.entry.value=Value
+customDb.export=Export
+customDb.reset=Reset
+customDb.url=URL
+main.menu.title=Databases
+main.menu.title.alt=Course databases
+tool.name=Databases
diff --git a/src/main/java/org/olat/course/db/_spring/coursedbContext.xml b/src/main/java/org/olat/course/db/_spring/coursedbContext.xml
new file mode 100644
index 0000000000000000000000000000000000000000..07af74c1081fbfbbae2fd60d1b475301ff52354f
--- /dev/null
+++ b/src/main/java/org/olat/course/db/_spring/coursedbContext.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans 
+                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
+	
+	<bean id="courseDBManager" class="org.olat.course.db.impl.CourseDBManagerImpl">
+		<property name="persistedProperties">
+	  	<bean class="org.olat.core.configuration.PersistedProperties" scope="prototype" init-method="init" destroy-method="destroy" 
+	  		depends-on="coordinatorManager,org.olat.core.util.WebappHelper">
+	    	<constructor-arg index="0" ref="coordinatorManager"/>
+	    	<constructor-arg index="1" ref="courseDBManager" />
+	  	</bean>
+		</property>
+	</bean>
+
+	<!-- default configuration -->
+	<bean  class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+		<property name="targetObject" ref="courseDBManager" />
+		<property name="targetMethod" value="init" />
+		<property name="arguments">
+			<value>
+				enabled=${course.db.enabled}
+			</value>
+		</property>
+	</bean>
+
+	<bean id="courseDbWebService" class="org.olat.course.db.restapi.CourseDbWebService" />
+
+	<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean" depends-on="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+		<property name="targetObject" ref="org.olat.restapi.support.RestRegistrationService" />
+		<property name="targetMethod" value="addSingleton" />
+		<property name="arguments" ref="courseDbWebService"/>
+	</bean>
+                        
+</beans>
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.hbm.xml b/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.hbm.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e9c116c529791c2767b8f656f10b8775f8c6e654
--- /dev/null
+++ b/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.hbm.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping PUBLIC
+	"-//Hibernate/Hibernate Mapping DTD//EN"
+	"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
+
+<hibernate-mapping default-lazy="false">
+
+  <class name="org.olat.course.db.impl.CourseDBEntryImpl" table="o_co_db_entry">
+    <id name="key" type="long" column="id" unsaved-value="null">
+      <generator class="hilo" />
+    </id>
+	
+		<version name="version" access="field" column="version"/>
+		<property  name="lastModified" column="lastmodified" type="timestamp" />
+		<property  name="creationDate" column="creationdate" type="timestamp" />
+
+    <property name="courseKey" type="long" column="courseid" />
+		<many-to-one
+			name="identity"
+			class="org.olat.basesecurity.IdentityImpl"
+			outer-join="false"
+			cascade="none"/>
+    
+    <property name="category"	type="string" column="category" length="32" index="o_co_db_cat_idx"/>
+    <property name="name"	type="string" column="name" not-null="true" index="o_co_db_name_idx"/>
+    <property name="floatValue"	type="float" column="floatvalue" />
+    <property name="longValue" type="long" column="longvalue" />
+    <property name="stringValue" type="string" column="stringvalue"	/>
+    <property name="textValue" type="text" column="textvalue" />
+  </class>
+
+</hibernate-mapping>
diff --git a/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.java b/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ccff8ffe318e6738a6100883bd702d2dcc0f8ec4
--- /dev/null
+++ b/src/main/java/org/olat/course/db/impl/CourseDBEntryImpl.java
@@ -0,0 +1,151 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db.impl;
+
+import java.util.Date;
+
+import org.olat.core.commons.persistence.PersistentObject;
+import org.olat.core.id.Identity;
+import org.olat.course.db.CourseDBEntry;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CourseDBEntryImpl
+ * 
+ * <P>
+ * Initial Date:  7 avr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public class CourseDBEntryImpl extends PersistentObject implements CourseDBEntry {
+	
+	private static final long serialVersionUID = -6487632477815812235L;
+	
+	private Long courseKey;
+	private Identity identity;
+	
+	private String category;
+	private String name;
+	private Float floatValue;
+	private Long 	longValue;
+	private String stringValue;
+	private String textValue;
+	private Date lastModified;
+	
+	public Long getCourseKey() {
+		return courseKey;
+	}
+	
+	public void setCourseKey(Long courseKey) {
+		this.courseKey = courseKey;
+	}
+	
+	public Identity getIdentity() {
+		return identity;
+	}
+	
+	public void setIdentity(Identity identity) {
+		this.identity = identity;
+	}
+	
+	public String getCategory() {
+		return category;
+	}
+	
+	public void setCategory(String category) {
+		this.category = category;
+	}
+	
+	public String getName() {
+		return name;
+	}
+	
+	public void setName(String name) {
+		this.name = name;
+	}
+	
+	@Override
+	public Object getValue() {
+		if(getStringValue() != null) {
+			return getStringValue();
+		} else if (getLongValue() != null) {
+			return getLongValue();
+		} else if (getFloatValue() != null) {
+			return getFloatValue();
+		}
+		return null;
+	}
+
+	@Override
+	public void setValue(Object value) {
+		if(value instanceof Long) {
+			setLongValue((Long)value);
+		} else if (value instanceof Float) {
+			setFloatValue((Float)value);
+		} else if (value instanceof String) {
+			setStringValue((String)value);
+		}
+	}
+
+	public Float getFloatValue() {
+		return floatValue;
+	}
+	
+	public void setFloatValue(Float floatValue) {
+		this.floatValue = floatValue;
+	}
+	
+	public Long getLongValue() {
+		return longValue;
+	}
+	
+	public void setLongValue(Long longValue) {
+		this.longValue = longValue;
+	}
+	
+	public String getStringValue() {
+		return stringValue;
+	}
+	
+	public void setStringValue(String stringValue) {
+		this.stringValue = stringValue;
+	}
+	
+	public String getTextValue() {
+		return textValue;
+	}
+	
+	public void setTextValue(String textValue) {
+		this.textValue = textValue;
+	}
+	
+	public Date getLastModified() {
+		return lastModified;
+	}
+	
+	public void setLastModified(Date lastModified) {
+		this.lastModified = lastModified;
+	}
+	
+	
+	
+
+}
diff --git a/src/main/java/org/olat/course/db/impl/CourseDBManagerImpl.java b/src/main/java/org/olat/course/db/impl/CourseDBManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..82d87ed7d0326685c4dda8366754bcf2368ad4b2
--- /dev/null
+++ b/src/main/java/org/olat/course/db/impl/CourseDBManagerImpl.java
@@ -0,0 +1,192 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db.impl;
+
+import java.util.List;
+
+import org.hibernate.FlushMode;
+import org.olat.core.commons.persistence.DBFactory;
+import org.olat.core.commons.persistence.DBQuery;
+import org.olat.core.configuration.PersistedProperties;
+import org.olat.core.id.Identity;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.event.GenericEventListener;
+import org.olat.course.ICourse;
+import org.olat.course.db.CourseDBEntry;
+import org.olat.course.db.CourseDBManager;
+
+/**
+ * 
+ * Description:<br>
+ * TODO: srosse Class Description for CourseDBManagerImpl
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+public class CourseDBManagerImpl extends CourseDBManager implements GenericEventListener {
+	
+	private static final String ENABLED = "enabled";
+	private boolean enabled;
+	
+	public CourseDBManagerImpl() {
+		//
+	}
+
+	@Override
+	public void init() {
+		//enabled/disabled
+		String enabledObj = getStringPropertyValue(ENABLED, true);
+		if(StringHelper.containsNonWhitespace(enabledObj)) {
+			enabled = "true".equals(enabledObj);
+		}
+	}
+
+	@Override
+	protected void initDefaultProperties() {
+		enabled = getBooleanConfigParameter("enabled", false);
+	}
+
+	@Override
+	protected void initFromChangedProperties() {
+		init();
+	}
+	
+	@Override
+	public void setPersistedProperties(PersistedProperties persistedProperties) {
+		this.moduleConfigProperties = persistedProperties;
+	}
+
+	@Override
+	public boolean isEnabled() {
+		return enabled;
+	}
+
+	@Override
+	public void reset(ICourse course, String category) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("delete from ").append(CourseDBEntryImpl.class.getName())
+			.append(" entry where entry.courseKey=:courseKey");
+		if(StringHelper.containsNonWhitespace(category)) {
+			sb.append(" and entry.category=:category");
+		}
+		
+		DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
+		query.setLong("courseKey", course.getResourceableId());
+		if(StringHelper.containsNonWhitespace(category)) {
+			query.setString("category", category);
+		}
+		query.executeUpdate(FlushMode.AUTO);
+	}
+	
+	@Override
+	public CourseDBEntry getValue(ICourse course, Identity identity, String category, String name) {
+		CourseDBEntry entry = loadEntry(course.getResourceableId(), identity, category, name);
+		return entry;
+	}
+
+	@Override
+	public CourseDBEntry getValue(Long courseResourceableId, Identity identity, String category, String name) {
+		CourseDBEntry entry = loadEntry(courseResourceableId, identity, category, name);
+		return entry;
+	}
+
+	@Override
+	public List<CourseDBEntry> getValues(ICourse course, Identity identity, String category, String name) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select entry from ").append(CourseDBEntryImpl.class.getName())
+			.append(" entry where entry.courseKey=:courseKey");
+		if(identity != null) {
+			sb.append(" and entry.identity=:identity");
+		}
+		if(StringHelper.containsNonWhitespace(category)) {
+			sb.append(" and entry.category=:category");
+		}
+		if(StringHelper.containsNonWhitespace(name)) {
+			sb.append(" and entry.name=:name");
+		}
+
+		DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
+		query.setLong("courseKey", course.getResourceableId());
+		if(identity != null) {
+			query.setEntity("identity", identity);
+		}
+		if(StringHelper.containsNonWhitespace(category)) {
+			query.setString("category", category);
+		}
+		if(StringHelper.containsNonWhitespace(name)) {
+			query.setString("name", name);
+		}
+		return query.list();
+	}
+
+	@Override
+	public CourseDBEntry setValue(ICourse course, Identity identity, String category, String name, Object value) {
+		CourseDBEntryImpl entry = loadEntry(course.getResourceableId(), identity, category, name);
+		if(entry == null) {
+			entry = new CourseDBEntryImpl();
+			entry.setCourseKey(course.getResourceableId());
+			entry.setIdentity(identity);
+			entry.setName(name);
+			entry.setCategory(category);
+		}
+		entry.setValue(value);
+		DBFactory.getInstance().saveObject(entry);
+		return entry;
+	}
+	
+	private CourseDBEntryImpl loadEntry(Long courseResourceableId, Identity identity, String category, String name) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select entry from ").append(CourseDBEntryImpl.class.getName())
+			.append(" entry where")
+			.append(" entry.identity=:identity and entry.courseKey=:courseKey ")
+			.append(" and entry.name=:named and entry.category=:category");
+
+		DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
+		query.setString("named", name);
+		query.setString("category", category);
+		query.setEntity("identity", identity);
+		query.setLong("courseKey", courseResourceableId);
+		
+		List<CourseDBEntryImpl> entries = query.list();
+		if(entries.isEmpty()) {
+			return null;
+		}
+		return entries.get(0);
+	}
+
+	@Override
+	public boolean deleteValue(ICourse course, Identity identity, String category, String name) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("delete from ").append(CourseDBEntryImpl.class.getName())
+			.append(" entry where entry.identity=:identity and entry.courseKey=:courseKey ")
+			.append(" and entry.name=:name and entry.category=:category");
+
+		DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
+		query.setString("name", name);
+		query.setString("category", category);
+		query.setEntity("identity", identity);
+		query.setLong("courseKey", course.getResourceableId());
+		
+		int rowAffected = query.executeUpdate(FlushMode.AUTO);
+		return rowAffected > 0;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/db/interpreter/GetUserCourseDBFunction.java b/src/main/java/org/olat/course/db/interpreter/GetUserCourseDBFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..2757d529a7948b73e95a82271c2a95c9a6b9f320
--- /dev/null
+++ b/src/main/java/org/olat/course/db/interpreter/GetUserCourseDBFunction.java
@@ -0,0 +1,125 @@
+/**
+ * <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.db.interpreter;
+
+import org.olat.core.id.Identity;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.course.condition.interpreter.AbstractFunction;
+import org.olat.course.condition.interpreter.ArgumentParseException;
+import org.olat.course.db.CourseDBEntry;
+import org.olat.course.db.CourseDBManager;
+import org.olat.course.editor.CourseEditorEnv;
+import org.olat.course.run.userview.UserCourseEnvironment;
+
+/**
+ * 
+ * Description:<br>
+ * 
+ * <P>
+ * Initial Date:  13 déc. 2011 <br>
+ *
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public class GetUserCourseDBFunction extends AbstractFunction {
+	
+	private static OLog log = Tracing.createLoggerFor(GetUserCourseDBFunction.class);
+
+	public static final String name = "getUserCourseDBValue";
+
+	/**
+	 * Constructor
+	 * @param userCourseEnv
+	 */
+	public GetUserCourseDBFunction(UserCourseEnvironment userCourseEnv) {
+		super(userCourseEnv);
+	}
+
+	/**
+	 * @see org.olat.course.condition.interpreter.AbstractFunction#call(java.lang.Object[])
+	 */
+	@Override
+	public Object call(Object[] inStack) {
+		/*
+		 * argument check
+		 */
+		if (inStack.length > 2) {
+			return handleException(new ArgumentParseException(ArgumentParseException.NEEDS_FEWER_ARGUMENTS, name, "", "error.fewerargs",
+					"solution.providetwo.attrvalue"));
+		} else if (inStack.length < 1) { 
+			return handleException(new ArgumentParseException(ArgumentParseException.NEEDS_MORE_ARGUMENTS, name,
+				"", "error.moreargs", "solution.providetwo.attrvalue")); 
+		}
+		/*
+		 * argument type check
+		 */
+		if (!(inStack[0] instanceof String)) { 
+			return handleException(new ArgumentParseException(ArgumentParseException.WRONG_ARGUMENT_FORMAT,
+				name, "", "error.argtype.attributename", "solution.example.name.infunction")); 
+		}
+		
+		CourseEditorEnv cev = getUserCourseEnv().getCourseEditorEnv();
+		if (cev != null) {
+			// return emtyp string to continue with condition evaluation test
+			return defaultValue();
+		}
+		
+		CourseDBManager courseDbManager = CourseDBManager.getInstance();
+
+		Identity ident = getUserCourseEnv().getIdentityEnvironment().getIdentity();
+		
+		String category = null;
+		String name = null;
+		if(inStack.length == 1) {
+			category = null;
+			name = (String) inStack[1];
+		} else if (inStack.length == 2) {
+			category = (String) inStack[0];
+			name = (String) inStack[1];
+		}
+		
+		Long courseId = getUserCourseEnv().getCourseEnvironment().getCourseResourceableId();
+		Object value;
+		try {
+			CourseDBEntry entry = courseDbManager.getValue(courseId, ident, category, name);
+			if(entry == null) {
+				return defaultValue();
+			}
+			value = entry.getValue();
+			if(value == null) {
+				return defaultValue();
+			}
+		} catch (Exception e) {
+			log.error("", e);
+			return defaultValue();
+		}
+		return value.toString();
+	}
+
+	/**
+	 * @see org.olat.course.condition.interpreter.AbstractFunction#defaultValue()
+	 */
+	@Override
+	protected Object defaultValue() {
+		return "";
+	}
+	
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/db/restapi/CourseDbWebService.java b/src/main/java/org/olat/course/db/restapi/CourseDbWebService.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4bebcee809d52468f538222740eecf042ff715d
--- /dev/null
+++ b/src/main/java/org/olat/course/db/restapi/CourseDbWebService.java
@@ -0,0 +1,296 @@
+/**
+ * OLAT - Online Learning and Training<br>
+ * http://www.olat.org
+ * <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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <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>
+ * Copyright (c) frentix GmbH<br>
+ * http://www.frentix.com<br>
+ * <p>
+ */
+package org.olat.course.db.restapi;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.id.Roles;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.db.CourseDBEntry;
+import org.olat.course.db.CourseDBManager;
+import org.olat.restapi.security.RestSecurityHelper;
+import org.olat.restapi.support.vo.KeyValuePair;
+
+/**
+ * Description:<br>
+ * Access the custom dbs of a course
+ * 
+ * <P>
+ * Initial Date:  7 apr. 2010 <br>
+ * @author srosse, stephane.rosse@frentix.com
+ */
+@Path("repo/courses/{courseId}/db/{category}")
+public class CourseDbWebService {
+	
+	private static final String VERSION  = "1.0";
+	
+	/**
+	 * Retrieves the version of the Course DB Web Service.
+   * @response.representation.200.mediaType text/plain
+   * @response.representation.200.doc The version of this specific Web Service
+   * @response.representation.200.example 1.0
+	 * @return
+	 */
+	@GET
+	@Path("version")
+	@Produces(MediaType.TEXT_PLAIN)
+	public Response getVersion() {
+		return Response.ok(VERSION).build();
+	}
+	
+	/**
+	 * Retrieve all values of the authenticated user
+	 * @response.representation.200.qname {http://www.example.com}keyValuePair
+   * @response.representation.200.mediaType application/xml, application/json
+   * @response.representation.200.doc All the values in the course
+   * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_KEYVALUEVOes}
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@GET
+	@Path("values")
+	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+	public Response getValues(@PathParam("courseId") Long courseId, @PathParam("category") String category, @Context HttpServletRequest request) {
+		ICourse course = CourseFactory.loadCourse(courseId);
+		UserRequest ureq = RestSecurityHelper.getUserRequest(request);
+		List<CourseDBEntry> entries = CourseDBManager.getInstance().getValues(course, ureq.getIdentity(), category, null);
+
+		KeyValuePair[] pairs = new KeyValuePair[entries.size()];
+		int count=0;
+		for(CourseDBEntry entry:entries) {
+			Object value = entry.getValue();
+			pairs[count++] = new KeyValuePair(entry.getName(), value == null ? "" : value.toString());
+		}
+		return Response.ok(pairs).build();
+	}
+	
+	/**
+	 * Put a new value for an authenticated user.
+	 * @response.representation.qname {http://www.example.com}keyValuePair
+   * @response.representation.mediaType application/xml, application/json
+   * @response.representation.doc the key value pair is saved on the db
+   * @response.representation.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_KEYVALUEVOes}
+   * @response.representation.200.doc the key value pair is saved on the db
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param pair The key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@PUT
+	@Path("values")
+	@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+	public Response putValues(@PathParam("courseId") Long courseId, @PathParam("category") String category, KeyValuePair pair, @Context HttpServletRequest request) {
+		return internPutValues(courseId, category, pair, request);
+	}
+	
+	/**
+	 * Update a value for an authenticated user.
+	 * @response.representation.qname {http://www.example.com}keyValuePair
+   * @response.representation.mediaType application/xml, application/json
+   * @response.representation.doc the key value pair is saved on the db
+   * @response.representation.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_KEYVALUEVOes}
+   * @response.representation.200.doc the key value pair is saved on the db
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param pair The key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@POST
+	@Path("values")
+	@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+	public Response postValues(@PathParam("courseId") Long courseId, @PathParam("category") String category, KeyValuePair pair, @Context HttpServletRequest request) {
+		return internPutValues(courseId, category, pair, request);
+	}
+
+	/**
+	 * Retrieve a value of an authenticated user.
+	 * @response.representation.200.qname {http://www.example.com}keyValuePair
+   * @response.representation.200.mediaType application/xml, application/json
+   * @response.representation.200.doc The value in the course
+   * @response.representation.200.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_KEYVALUEVO}
+	 * @response.representation.404.doc The entry cannot be found
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @parma name The name of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@GET
+	@Path("values/{name}")
+	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+	public Response getValue(@PathParam("courseId") Long courseId, @PathParam("category") String category, @PathParam("name") String name, @Context HttpServletRequest request) {
+		ICourse course = CourseFactory.loadCourse(courseId);
+		UserRequest ureq = RestSecurityHelper.getUserRequest(request);
+		CourseDBEntry entry = CourseDBManager.getInstance().getValue(course, ureq.getIdentity(), category, name);
+		if(entry == null) {
+			return Response.serverError().status(Status.NOT_FOUND).build();
+		}
+		Object value = entry.getValue();
+		KeyValuePair pair = new KeyValuePair(name, value == null ? "" : value.toString());
+		return Response.ok(pair).build();
+	}
+
+	/**
+	 * Retrieve a value of an authenticated user.
+	 * @response.representation.200.qname {http://www.example.com}keyValuePair
+   * @response.representation.200.mediaType text/plain, text/html
+   * @response.representation.200.doc A value of the course
+   * @response.representation.200.example Green
+	 * @response.representation.404.doc The entry cannot be found
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param name The name of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@GET
+	@Path("values/{name}")
+	@Produces({MediaType.TEXT_PLAIN, MediaType.TEXT_HTML})
+	public Response getValuePlain(@PathParam("courseId") Long courseId, @PathParam("category") String category, @PathParam("name") String name,
+			@Context HttpServletRequest request) {
+		ICourse course = CourseFactory.loadCourse(courseId);
+		UserRequest ureq = RestSecurityHelper.getUserRequest(request);
+		CourseDBEntry entry = CourseDBManager.getInstance().getValue(course, ureq.getIdentity(), category, name);
+		if(entry == null) {
+			return Response.serverError().status(Status.NOT_FOUND).build();
+		}
+		Object value = entry.getValue();
+		String val = value == null ? "" : value.toString();
+		return Response.ok(val).build();
+	}
+
+	/**
+	 * Put a new value for an authenticated user.
+   * @response.representation.200.doc The value is saved in the course
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param name The name of the key value pair
+	 * @param value The value of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@PUT
+	@Path("values/{name}")
+	public Response putValue(@PathParam("courseId") Long courseId, @PathParam("category") String category, @PathParam("name") String name,
+			@QueryParam("value") String value, @Context HttpServletRequest request) {
+		return internPutValue(courseId, category, name, value, request);
+	}
+
+	/**
+	 * Update a value for an authenticated user.
+   * @response.representation.200.doc The value is saved in the course
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param name The name of the key value pair
+	 * @param val The value of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@POST
+	@Path("values/{name}")
+	@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+	public Response formValue(@PathParam("courseId") Long courseId, @PathParam("category") String category, @PathParam("name") String name,
+			@FormParam("val") String value, @Context HttpServletRequest request){
+		return internPutValue(courseId, category, name, value, request);
+	}
+	
+	/**
+	 * Delete a value for an authenticated user.
+   * @response.representation.200.doc the key value pair is remove from the db
+	 * @response.representation.401.doc The roles of the authenticated user are not sufficient
+	 * @response.representation.404.doc The entry cannot be found
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param name The name of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@DELETE
+	@Path("values/{name}")
+	public Response deleteValue(@PathParam("courseId") Long courseId, @PathParam("category") String category, 
+			@PathParam("name") String name, @Context HttpServletRequest request) {
+		Roles roles = RestSecurityHelper.getRoles(request);
+		if(roles.isAuthor() || roles.isOLATAdmin()) {
+			ICourse course = CourseFactory.loadCourse(courseId);
+			UserRequest ureq = RestSecurityHelper.getUserRequest(request);
+			boolean ok = CourseDBManager.getInstance().deleteValue(course, ureq.getIdentity(), category, name);
+			if(ok) {
+				return Response.ok().build();
+			}
+			return Response.serverError().status(Status.NOT_FOUND).build();
+		}
+		return Response.serverError().status(Status.UNAUTHORIZED).build();
+	}
+	
+	/**
+	 * Fallbakc method for the browsers
+   * @response.representation.200.doc the key value pair is remove from the db
+	 * @response.representation.401.doc The roles of the authenticated user are not sufficient
+	 * @response.representation.404.doc The entry cannot be found
+	 * @param courseId The course resourceable's id
+	 * @param category The name of the database
+	 * @param name The name of the key value pair
+	 * @param request The HTTP request
+	 * @return
+	 */
+	@POST
+	@Path("values/{name}/delete")
+	public Response deleteValuePost(@PathParam("courseId") Long courseId, @PathParam("category") String category,
+			@PathParam("name") String name, @Context HttpServletRequest request) {
+		return deleteValue(courseId, category, name, request);
+	}
+	
+	private Response internPutValues(Long courseId, String category, KeyValuePair pair, HttpServletRequest request) {
+		return internPutValue(courseId, category, pair.getKey(), pair.getValue(), request);
+	}
+	
+	private Response internPutValue(Long courseId, String category, String name, Object value, HttpServletRequest request) {
+		ICourse course = CourseFactory.loadCourse(courseId);
+		UserRequest ureq = RestSecurityHelper.getUserRequest(request);
+		CourseDBEntry entry = CourseDBManager.getInstance().setValue(course, ureq.getIdentity(), category, name, value);
+		if(entry == null) {
+			return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
+		}
+		return Response.ok().build();
+	}
+}
diff --git a/src/main/java/org/olat/course/groupsandrights/CourseRights.java b/src/main/java/org/olat/course/groupsandrights/CourseRights.java
index c55238ad8764092137bbaacb69006f5e5e9fe8f7..a4eb12c56c621115eb3d0048f9c95bb65f22d4e9 100644
--- a/src/main/java/org/olat/course/groupsandrights/CourseRights.java
+++ b/src/main/java/org/olat/course/groupsandrights/CourseRights.java
@@ -59,6 +59,9 @@ public class CourseRights implements BGRights {
     public static final String RIGHT_GLOSSARY = BGRightManager.BG_RIGHT_PREFIX + "glossary";
     /** course right for statistics tool */
     public static final String RIGHT_STATISTICS = BGRightManager.BG_RIGHT_PREFIX + "statistics";
+    //fxdiff: right for course db
+    /** course right for custom dbs */
+    public static final String RIGHT_DB = BGRightManager.BG_RIGHT_PREFIX + "dbs";
     
     private static List rights;
     private Translator trans;
@@ -72,6 +75,8 @@ public class CourseRights implements BGRights {
         rights.add(RIGHT_ASSESSMENT);
         rights.add(RIGHT_GLOSSARY);
         rights.add(RIGHT_STATISTICS);
+        //fxdiff: right for course db
+        rights.add(RIGHT_DB);
     }
     
    
diff --git a/src/main/java/org/olat/course/run/RunMainController.java b/src/main/java/org/olat/course/run/RunMainController.java
index c6ce6a52806bd4cf168a113148790ea09bf949c0..cd4a238a62ccba1a02323eb5f5bfa996eeb72613 100644
--- a/src/main/java/org/olat/course/run/RunMainController.java
+++ b/src/main/java/org/olat/course/run/RunMainController.java
@@ -101,6 +101,8 @@ import org.olat.course.assessment.UserEfficiencyStatement;
 import org.olat.course.assessment.manager.UserCourseInformationsManager;
 import org.olat.course.config.CourseConfig;
 import org.olat.course.config.CourseConfigEvent;
+import org.olat.course.db.CourseDBManager;
+import org.olat.course.db.CustomDBMainController;
 import org.olat.course.editor.PublishEvent;
 import org.olat.course.groupsandrights.CourseGroupManager;
 import org.olat.course.groupsandrights.CourseRights;
@@ -502,6 +504,7 @@ public class RunMainController extends MainLayoutBasicController implements Gene
 		courseRightsCache.put(CourseRights.RIGHT_ASSESSMENT, new Boolean(cgm.hasRight(identity, CourseRights.RIGHT_ASSESSMENT)));
 		courseRightsCache.put(CourseRights.RIGHT_GLOSSARY, new Boolean(cgm.hasRight(identity, CourseRights.RIGHT_GLOSSARY)));
 		courseRightsCache.put(CourseRights.RIGHT_STATISTICS, new Boolean(cgm.hasRight(identity, CourseRights.RIGHT_STATISTICS)));
+		courseRightsCache.put(CourseRights.RIGHT_DB, new Boolean(cgm.hasRight(identity, CourseRights.RIGHT_DB)));
 	}
 
 	/**
@@ -841,7 +844,15 @@ public class RunMainController extends MainLayoutBasicController implements Gene
 				listenTo(currentToolCtr);
 				all.setContent(currentToolCtr.getInitialComponent());
 			} else throw new OLATSecurityException("clicked statistic, but no according right");
-		} else if (cmd.equals("archiver")) {
+		//fxdiff: open a panel to manage the course dbs
+		} else if (cmd.equals("customDb")) {
+			if (hasCourseRight(CourseRights.RIGHT_DB) || isCourseAdmin) {
+				currentToolCtr = new CustomDBMainController(ureq, getWindowControl(), course);
+				listenTo(currentToolCtr);
+				all.setContent(currentToolCtr.getInitialComponent());
+			} else throw new OLATSecurityException("clicked dbs, but no according right");
+
+		}else if (cmd.equals("archiver")) {
 			if (hasCourseRight(CourseRights.RIGHT_ARCHIVING) || isCourseAdmin) {
 				currentToolCtr = new ArchiverMainController(ureq, getWindowControl(), course, new IArchiverCallback() {
 					public boolean mayArchiveQtiResults() {
@@ -1148,6 +1159,10 @@ public class RunMainController extends MainLayoutBasicController implements Gene
 			if (hasCourseRight(CourseRights.RIGHT_STATISTICS) || isCourseAdmin) {
 				myTool.addLink("statistic", translate("command.openstatistic"));
 			}
+			//fxdiff: enable the course db menu item
+			if (CourseDBManager.getInstance().isEnabled() && (hasCourseRight(CourseRights.RIGHT_DB) || isCourseAdmin)) {
+				myTool.addLink("customDb", translate("command.opendb"));
+			}
 
 			//
 			/*
diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties
index 06c2d954b6df3b01c57a7f245e6538d88c76f085..c460bf10ff25e143f29d0dcedd93765c269e49fc 100644
--- a/src/main/resources/serviceconfig/olat.properties
+++ b/src/main/resources/serviceconfig/olat.properties
@@ -320,6 +320,8 @@ minimalhome.ext.calendar=true
 minimalhome.ext.mysettings=true
 minimalhome.ext.portal=true
 
+course.db.enabled=false
+
 ########################################################################
 #Top navigation configuration
 ########################################################################