From a72b103bdbc1646a227b0c696ac3f25c1807112e Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 8 Jul 2016 17:11:56 +0200
Subject: [PATCH] OO-2057: copy / import / export binder template

---
 .../modules/portfolio/PortfolioService.java   |   6 +
 .../handler/BinderTemplateHandler.java        |  96 ++++++++++++---
 .../handler/BinderTemplateMediaResource.java  | 113 ++++++++++++++++++
 .../handler/BinderTemplateResource.java       |  64 +++++++++-
 .../portfolio/handler/BinderXStream.java      |  40 ++++---
 .../modules/portfolio/manager/BinderDAO.java  |  20 ++++
 .../manager/PortfolioServiceImpl.java         |  42 +++++++
 .../ui/_i18n/LocalStrings_de.properties       |   1 +
 .../ui/_i18n/LocalStrings_en.properties       |   1 +
 .../RepositoryEntryImportExport.java          |  36 ++++++
 .../handlers/RepositoryHandler.java           |   2 +-
 11 files changed, 384 insertions(+), 37 deletions(-)
 create mode 100644 src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java

diff --git a/src/main/java/org/olat/modules/portfolio/PortfolioService.java b/src/main/java/org/olat/modules/portfolio/PortfolioService.java
index f69d1ef6ec2..49ffc766d29 100644
--- a/src/main/java/org/olat/modules/portfolio/PortfolioService.java
+++ b/src/main/java/org/olat/modules/portfolio/PortfolioService.java
@@ -54,6 +54,12 @@ public interface PortfolioService {
 	
 	public Binder updateBinder(Binder binder);
 	
+	public Binder copyBinder(Binder transientBinder, RepositoryEntry templateEntry);
+	
+	public Binder importBinder(Binder transientBinder, RepositoryEntry templateEntry, File image);
+	
+	public boolean deleteBinderTemplate(Binder binder, RepositoryEntry templateEntry);
+	
 	/**
 	 * Add a new section at the end of the sections list of the specified binder.
 	 * 
diff --git a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateHandler.java b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateHandler.java
index c6a524ccc21..3ea96c6914d 100644
--- a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateHandler.java
+++ b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateHandler.java
@@ -19,12 +19,12 @@
  */
 package org.olat.modules.portfolio.handler;
 
-import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.util.Locale;
 
 import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
 import org.olat.core.commons.persistence.DBFactory;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.stack.TooledStackedPanel;
@@ -33,19 +33,23 @@ import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.generic.layout.MainLayoutController;
 import org.olat.core.gui.control.generic.wizard.StepsMainRunController;
 import org.olat.core.gui.media.MediaResource;
-import org.olat.core.gui.media.StreamedMediaResource;
+import org.olat.core.gui.translator.Translator;
 import org.olat.core.id.Identity;
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.id.Roles;
 import org.olat.core.logging.AssertException;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.Util;
 import org.olat.core.util.coordinate.CoordinatorManager;
 import org.olat.core.util.coordinate.LockResult;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.course.assessment.AssessmentMode;
 import org.olat.course.assessment.manager.UserCourseInformationsManager;
 import org.olat.fileresource.FileResourceManager;
+import org.olat.fileresource.types.FileResource;
 import org.olat.fileresource.types.ResourceEvaluation;
 import org.olat.modules.portfolio.Binder;
 import org.olat.modules.portfolio.BinderConfiguration;
@@ -57,8 +61,10 @@ import org.olat.modules.portfolio.ui.BinderController;
 import org.olat.modules.portfolio.ui.BinderPickerController;
 import org.olat.modules.portfolio.ui.BinderRuntimeController;
 import org.olat.modules.portfolio.ui.PortfolioAssessmentDetailsController;
+import org.olat.modules.portfolio.ui.PortfolioHomeController;
 import org.olat.repository.ErrorList;
 import org.olat.repository.RepositoryEntry;
+import org.olat.repository.RepositoryEntryImportExport;
 import org.olat.repository.RepositoryManager;
 import org.olat.repository.RepositoryService;
 import org.olat.repository.handlers.EditionSupport;
@@ -66,6 +72,7 @@ import org.olat.repository.handlers.RepositoryHandler;
 import org.olat.repository.model.RepositoryEntrySecurity;
 import org.olat.repository.ui.RepositoryEntryRuntimeController.RuntimeControllerCreator;
 import org.olat.resource.OLATResource;
+import org.olat.resource.references.ReferenceManager;
 
 /**
  * 
@@ -80,7 +87,7 @@ import org.olat.resource.OLATResource;
 public class BinderTemplateHandler implements RepositoryHandler {
 	
 	private static final OLog log = Tracing.createLoggerFor(BinderTemplateHandler.class);
-	
+
 	@Override
 	public boolean isCreate() {
 		return CoreSpringFactory.getImpl(PortfolioV2Module.class).isEnabled();
@@ -109,18 +116,56 @@ public class BinderTemplateHandler implements RepositoryHandler {
 
 	@Override
 	public ResourceEvaluation acceptImport(File file, String filename) {
-		return new ResourceEvaluation(false);
+		return BinderTemplateResource.evaluate(file, filename);
 	}
 	
 	@Override
 	public RepositoryEntry importResource(Identity initialAuthor, String initialAuthorAlt, String displayname, String description,
 			boolean withReferences, Locale locale, File file, String filename) {
-		return null;
+		
+		RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class);
+		PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
+		try {
+			//create resource
+			OLATResource resource = portfolioService.createBinderTemplateResource();
+			OlatRootFolderImpl fResourceRootContainer = FileResourceManager.getInstance().getFileResourceRootImpl(resource);
+			File fResourceFileroot = fResourceRootContainer.getBasefile();
+			File zipRoot = new File(fResourceFileroot, FileResourceManager.ZIPDIR);
+			FileResource.copyResource(file, filename, zipRoot);
+
+			//create repository entry
+			RepositoryEntry re = repositoryService.create(initialAuthor, initialAuthorAlt, "", displayname, description, resource, RepositoryEntry.ACC_OWNERS);
+			
+			//import binder
+			File binderFile = new File(zipRoot, BinderTemplateResource.BINDER_XML);
+			Binder transientBinder = BinderXStream.fromPath(binderFile.toPath());
+			
+			File posterImage = null;
+			if(StringHelper.containsNonWhitespace(transientBinder.getImagePath())) {
+				posterImage = new File(zipRoot, transientBinder.getImagePath());
+			}
+			portfolioService.importBinder(transientBinder, re, posterImage);
+
+			RepositoryEntryImportExport rei = new RepositoryEntryImportExport(re, zipRoot);
+			if(rei.anyExportedPropertiesAvailable()) {
+				re = rei.importContent(re, fResourceRootContainer.createChildContainer("media"));
+			}
+			//delete the imported files
+			FileUtils.deleteDirsAndFiles(zipRoot, true, true);
+			return re;
+		} catch (IOException e) {
+			log.error("", e);
+			return null;
+		}
 	}
 	
 	@Override
 	public RepositoryEntry copy(Identity author, RepositoryEntry source, RepositoryEntry target) {
-		return null;
+		PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
+		Binder templateSource = portfolioService.getBinderByResource(source.getOlatResource());
+		Binder transientCopy = BinderXStream.copy(templateSource);
+		portfolioService.copyBinder(transientCopy, target);
+		return target;
 	}
 
 	@Override
@@ -146,12 +191,32 @@ public class BinderTemplateHandler implements RepositoryHandler {
 
 	@Override
 	public boolean readyToDelete(RepositoryEntry entry, Identity identity, Roles roles, Locale locale, ErrorList errors) {
-		return false;
+		PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
+		Binder template = portfolioService.getBinderByResource(entry.getOlatResource());
+		if(portfolioService.isTemplateInUse(template, null, null)) {
+			Translator translator = Util.createPackageTranslator(PortfolioHomeController.class, locale);
+			errors.setError(translator.translate("warning.template.in.use",
+					new String[] { template.getTitle(), entry.getDisplayname() }));
+			return false;
+		}
+		
+		String referencesSummary = CoreSpringFactory.getImpl(ReferenceManager.class)
+				.getReferencesToSummary(entry.getOlatResource(), locale);
+		if (referencesSummary != null) {
+			Translator translator = Util.createPackageTranslator(RepositoryManager.class, locale);
+			errors.setError(translator.translate("details.delete.error.references",
+					new String[] { referencesSummary, entry.getDisplayname() }));
+			return false;
+		}
+		
+		return true;
 	}
 
 	@Override
 	public boolean cleanupOnDelete(RepositoryEntry entry, OLATResourceable res) {
-		return false;
+		PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
+		Binder template = portfolioService.getBinderByResource(entry.getOlatResource());
+		return portfolioService.deleteBinderTemplate(template, entry);
 	}
 
 	/**
@@ -160,17 +225,10 @@ public class BinderTemplateHandler implements RepositoryHandler {
 	 */
 	@Override
 	public MediaResource getAsMediaResource(OLATResourceable res, boolean backwardsCompatible) {
-		RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(res, true);
-		PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
-		Binder template = portfolioService.getBinderByResource(re.getOlatResource());
-		
-		try {
-			byte[] bytes = BinderXStream.toBytes(template);
-			return new StreamedMediaResource(new ByteArrayInputStream(bytes), "binder.zip", "application/zip", new Long(bytes.length), null);
-		} catch (IOException e) {
-			log.error("", e);
-			return null;
-		}
+		RepositoryEntry templateEntry = RepositoryManager.getInstance().lookupRepositoryEntry(res, true);
+		Binder template = CoreSpringFactory.getImpl(PortfolioService.class)
+				.getBinderByResource(templateEntry.getOlatResource());
+		return new BinderTemplateMediaResource(template, templateEntry);
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java
new file mode 100644
index 00000000000..63e0efcaba8
--- /dev/null
+++ b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateMediaResource.java
@@ -0,0 +1,113 @@
+package org.olat.modules.portfolio.handler;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FileUtils;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.io.ShieldOutputStream;
+import org.olat.fileresource.FileResourceManager;
+import org.olat.modules.portfolio.Binder;
+import org.olat.modules.portfolio.BinderRef;
+import org.olat.modules.portfolio.PortfolioService;
+import org.olat.repository.RepositoryEntry;
+import org.olat.repository.RepositoryEntryImportExport;
+import org.olat.resource.OLATResource;
+
+/**
+ * 
+ * Initial date: 08.07.2016<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class BinderTemplateMediaResource implements MediaResource {
+	
+	private static final OLog log = Tracing.createLoggerFor(BinderTemplateMediaResource.class);
+	
+	private final BinderRef template;
+	private final RepositoryEntry templateEntry;
+	
+	public BinderTemplateMediaResource(BinderRef template, RepositoryEntry templateEntry) {
+		this.template = template;
+		this.templateEntry = templateEntry;
+	}
+	
+	@Override
+	public boolean acceptRanges() {
+		return false;
+	}
+	
+	@Override
+	public String getContentType() {
+		return "application/zip";
+	}
+
+	@Override
+	public Long getSize() {
+		return null;
+	}
+
+	@Override
+	public InputStream getInputStream() {
+		return null;
+	}
+
+	@Override
+	public Long getLastModified() {
+		return null;
+	}
+
+	@Override
+	public void release() {
+		//
+	}
+	
+	@Override
+	public void prepare(HttpServletResponse hres) {
+		try {
+			hres.setCharacterEncoding("UTF-8");
+		} catch (Exception e) {
+			log.error("", e);
+		}
+		
+		try(ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream())) {
+			PortfolioService portfolioService = CoreSpringFactory.getImpl(PortfolioService.class);
+			Binder loadedTemplate = portfolioService.getBinderByKey(template.getKey());
+			String label = loadedTemplate.getTitle();
+			String secureLabel = StringHelper.transformDisplayNameToFileSystemName(label);
+
+			String file = secureLabel + ".zip";
+			hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(file));			
+			hres.setHeader("Content-Description", StringHelper.urlEncodeUTF8(label));
+
+			zout.setLevel(9);
+			zout.putNextEntry(new ZipEntry("binder.xml"));
+			BinderXStream.toStream(loadedTemplate, zout);
+			zout.closeEntry();
+			
+			if(StringHelper.containsNonWhitespace(loadedTemplate.getImagePath())) {
+				File posterImage = portfolioService.getPosterImageFile(loadedTemplate);
+				if(posterImage.exists()) {
+					zout.putNextEntry(new ZipEntry(loadedTemplate.getImagePath()));
+					FileUtils.copyFile(posterImage, new ShieldOutputStream(zout));
+					zout.closeEntry();
+				}
+			}
+			
+			OLATResource resource = templateEntry.getOlatResource();
+			File baseContainer= FileResourceManager.getInstance().getFileResource(resource);
+			RepositoryEntryImportExport importExport = new RepositoryEntryImportExport(templateEntry, baseContainer);
+			importExport.exportDoExportProperties(zout);
+		} catch (Exception e) {
+			log.error("", e);
+		}
+	}
+}
diff --git a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateResource.java b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateResource.java
index f5737b0303a..7eb16f26d0d 100644
--- a/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateResource.java
+++ b/src/main/java/org/olat/modules/portfolio/handler/BinderTemplateResource.java
@@ -20,8 +20,20 @@
 package org.olat.modules.portfolio.handler;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.PathUtils;
 import org.olat.fileresource.types.FileResource;
+import org.olat.fileresource.types.ResourceEvaluation;
+import org.olat.repository.RepositoryEntryImportExport;
+import org.olat.repository.RepositoryEntryImportExport.RepositoryEntryImport;
 
 /**
  * 
@@ -33,7 +45,10 @@ import org.olat.fileresource.types.FileResource;
  */
 public class BinderTemplateResource extends FileResource  {
 	
+	private static final OLog log = Tracing.createLoggerFor(BinderTemplateResource.class);
+	
 	public static final String TYPE_NAME = "BinderTemplate";
+	public static final String BINDER_XML = "binder.xml";
 
 	/**
 	 * @param f
@@ -42,8 +57,53 @@ public class BinderTemplateResource extends FileResource  {
 	public static boolean validate(File f) {
 		if(f.isDirectory()) {
 			//unzip directory
-			return new File(f, "map.xml").exists();
+			return new File(f, BINDER_XML).exists();
+		}
+		return f.getName().toLowerCase().endsWith(BINDER_XML); 
+	}
+	
+	public static ResourceEvaluation evaluate(File file, String filename) {
+		ResourceEvaluation eval = new ResourceEvaluation();
+		try {
+			BinderFileFilter visitor = new BinderFileFilter();
+			Path fPath = PathUtils.visit(file, filename, visitor);
+			
+			if(visitor.isValid()) {
+				eval.setValid(true);
+				Path repoXml = fPath.resolve(RepositoryEntryImportExport.PROPERTIES_FILE);
+				if(Files.exists(repoXml)) {
+					RepositoryEntryImport re = RepositoryEntryImportExport.getConfiguration(repoXml);
+					if(re != null) {
+						eval.setDisplayname(re.getDisplayname());
+						eval.setDescription(re.getDescription());
+					}
+				}
+			} else {
+				eval.setValid(false);
+			}
+		} catch (IOException e) {
+			log.error("", e);
+			eval.setValid(false);
+		}
+		return eval;
+	}
+	
+	private static class BinderFileFilter extends SimpleFileVisitor<Path> {
+		private boolean binderFile;
+
+		@Override
+		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+		throws IOException {
+
+			String filename = file.getFileName().toString();
+			if(BINDER_XML.equals(filename)) {
+				binderFile = true;
+			}
+			return binderFile ? FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
+		}
+		
+		public boolean isValid() {
+			return binderFile;
 		}
-		return f.getName().toLowerCase().endsWith("map.xml"); 
 	}
 }
diff --git a/src/main/java/org/olat/modules/portfolio/handler/BinderXStream.java b/src/main/java/org/olat/modules/portfolio/handler/BinderXStream.java
index f2dd5a97752..77e4e6f4562 100644
--- a/src/main/java/org/olat/modules/portfolio/handler/BinderXStream.java
+++ b/src/main/java/org/olat/modules/portfolio/handler/BinderXStream.java
@@ -1,12 +1,14 @@
 package org.olat.modules.portfolio.handler;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.util.zip.ZipEntry;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.zip.ZipOutputStream;
 
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
+import org.olat.core.util.io.ShieldOutputStream;
 import org.olat.core.util.xml.XStreamHelper;
 import org.olat.modules.portfolio.Binder;
 
@@ -24,21 +26,29 @@ public class BinderXStream {
 	private static final XStream myStream = XStreamHelper.createXStreamInstanceForDBObjects();
 	
 	
-	public static final byte[] toBytes(Binder binder)
+	
+	public static final Binder copy(Binder binder) {
+		String stringuified = myStream.toXML(binder);
+		Binder copiedBinder = (Binder)myStream.fromXML(stringuified);
+		return copiedBinder;
+	}
+	
+	public static final Binder fromPath(Path path)
+	throws IOException {	
+		try(InputStream inStream = Files.newInputStream(path)) {
+			return (Binder)myStream.fromXML(inStream);
+		} catch (Exception e) {
+			log.error("Cannot import this map: " + path, e);
+			return null;
+		}
+	}
+	
+	public static final void toStream(Binder binder, ZipOutputStream zout)
 	throws IOException {
-		try(ByteArrayOutputStream out = new ByteArrayOutputStream();
-				ZipOutputStream zipOut = new ZipOutputStream(out);) {
-			//prepare a zip
-			
-			zipOut.putNextEntry(new ZipEntry("binder.xml"));
-			myStream.toXML(binder, zipOut);
-			zipOut.closeEntry();
-			zipOut.close();
-
-			return out.toByteArray();
-		} catch (IOException e) {
+		try {
+			myStream.toXML(binder, new ShieldOutputStream(zout));
+		} catch (Exception e) {
 			log.error("Cannot export this map: " + binder, e);
-			return null;
 		}
 	}
 
diff --git a/src/main/java/org/olat/modules/portfolio/manager/BinderDAO.java b/src/main/java/org/olat/modules/portfolio/manager/BinderDAO.java
index 36c0ce0c9e4..a5b0d13b4d0 100644
--- a/src/main/java/org/olat/modules/portfolio/manager/BinderDAO.java
+++ b/src/main/java/org/olat/modules/portfolio/manager/BinderDAO.java
@@ -197,6 +197,26 @@ public class BinderDAO {
 			.getResultList();
 	}
 	
+	public void detachBinderTemplate() {
+		//unlink entry
+		//unlink template
+	}
+	
+	public int deleteBinderTemplate(Binder binder) {
+		String sectionQ = "delete from pfsection section where section.binder.key=:binderKey";
+		int sections = dbInstance.getCurrentEntityManager()
+				.createQuery(sectionQ)
+				.setParameter("binderKey", binder.getKey())
+				.executeUpdate();
+
+		String binderQ = "delete from pfbinder binder where binder.key=:binderKey";
+		int binders = dbInstance.getCurrentEntityManager()
+				.createQuery(binderQ)
+				.setParameter("binderKey", binder.getKey())
+				.executeUpdate();
+		return sections + binders;
+	}
+	
 	/**
 	 * The same type of query is user for the categories
 	 * @param owner
diff --git a/src/main/java/org/olat/modules/portfolio/manager/PortfolioServiceImpl.java b/src/main/java/org/olat/modules/portfolio/manager/PortfolioServiceImpl.java
index cf7dab3614e..2ae11cd9d86 100644
--- a/src/main/java/org/olat/modules/portfolio/manager/PortfolioServiceImpl.java
+++ b/src/main/java/org/olat/modules/portfolio/manager/PortfolioServiceImpl.java
@@ -162,6 +162,45 @@ public class PortfolioServiceImpl implements PortfolioService {
 		return binderDao.updateBinder(binder);
 	}
 
+	@Override
+	public Binder copyBinder(Binder transientBinder, RepositoryEntry entry) {
+		String imagePath = null;
+		if(StringHelper.containsNonWhitespace(transientBinder.getImagePath())) {
+			File bcroot = portfolioFileStorage.getRootDirectory();
+			File image = new File(bcroot, transientBinder.getImagePath());
+			if(image.exists()) {
+				imagePath = addPosterImageForBinder(image, image.getName());
+			}
+		}
+		return internalCopyTransientBinder(transientBinder, entry, imagePath);
+	}
+
+	@Override
+	public Binder importBinder(Binder transientBinder, RepositoryEntry templateEntry, File image) {
+		String imagePath = null;
+		if(StringHelper.containsNonWhitespace(transientBinder.getImagePath())) {
+			imagePath = addPosterImageForBinder(image, image.getName());
+		}
+		return internalCopyTransientBinder(transientBinder, templateEntry, imagePath);
+	}
+	
+	private Binder internalCopyTransientBinder(Binder transientBinder, RepositoryEntry entry, String imagePath) {
+		Binder binder = binderDao.createAndPersist(transientBinder.getTitle(), transientBinder.getSummary(), imagePath, entry);
+		//copy sections
+		for(Section transientSection:((BinderImpl)transientBinder).getSections()) {
+			binderDao.createSection(transientSection.getTitle(), transientSection.getDescription(),
+					transientSection.getBeginDate(), transientSection.getEndDate(), binder);
+		}
+		return binder;
+	}
+
+	@Override
+	public boolean deleteBinderTemplate(Binder binder, RepositoryEntry templateEntry) {
+		binderDao.detachBinderTemplate();
+		int deletedRows = binderDao.deleteBinderTemplate(binder);
+		return deletedRows > 0;
+	}
+
 	@Override
 	public void appendNewSection(String title, String description, Date begin, Date end, BinderRef binder) {
 		Binder reloadedBinder = binderDao.loadByKey(binder.getKey());
@@ -458,6 +497,9 @@ public class PortfolioServiceImpl implements PortfolioService {
 	@Override
 	public String addPosterImageForBinder(File file, String filename) {
 		File dir = portfolioFileStorage.generateBinderSubDirectory();
+		if(!StringHelper.containsNonWhitespace(filename)) {
+			filename = file.getName();
+		}
 		File destinationFile = new File(dir, filename);
 		String renamedFile = FileUtils.rename(destinationFile);
 		if(renamedFile != null) {
diff --git a/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_de.properties
index 3c88b5a94c3..52e0c8fae67 100644
--- a/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_de.properties
@@ -185,3 +185,4 @@ timeline.switch.on=Timeline
 timeline.switch.off=Timeline
 warning.binder.synched=Die Portfolio Mappe wurde mit seiner Vorlage synchroniziert.
 warning.portfolio.not.found=Die Portfolio Mappe konnte nicht gefunden werden. Sie wurde wahrscheinlich gel\u00F6scht.
+warning.template.in.use=Die Vorlage konnte nicht gelöscht werden weil einige Benutzer es nutzt.
diff --git a/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_en.properties
index 4814d2030ee..00006d2665e 100644
--- a/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/portfolio/ui/_i18n/LocalStrings_en.properties
@@ -184,4 +184,5 @@ title=Title
 timeline.switch.on=Timeline
 timeline.switch.off=Timeline
 warning.binder.synched=The binder is synchronized with its template.
+warning.template.in.use=The template cannot deleted because some users use it.
 warning.portfolio.not.found=The portfolio cannot be found, probably deleted in the mean time
diff --git a/src/main/java/org/olat/repository/RepositoryEntryImportExport.java b/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
index 423e326a164..b1ae92d834a 100644
--- a/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
+++ b/src/main/java/org/olat/repository/RepositoryEntryImportExport.java
@@ -34,6 +34,8 @@ import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 import javax.servlet.http.HttpServletResponse;
 
@@ -46,6 +48,7 @@ import org.olat.core.logging.Tracing;
 import org.olat.core.util.FileUtils;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.io.HttpServletResponseOutputStream;
+import org.olat.core.util.io.ShieldOutputStream;
 import org.olat.core.util.vfs.LocalFileImpl;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSLeaf;
@@ -153,6 +156,39 @@ public class RepositoryEntryImportExport {
 			FileUtils.closeSafely(fOut);
 		}
 	}
+	
+	public void exportDoExportProperties(ZipOutputStream zout) throws IOException {
+		RepositoryEntryImport imp = new RepositoryEntryImport(re);
+		RepositoryManager rm = RepositoryManager.getInstance();
+		VFSLeaf image = rm.getImage(re);
+		if(image != null) {
+			imp.setImageName(image.getName());
+			zout.putNextEntry(new ZipEntry(image.getName()));
+			try(InputStream inImage=image.getInputStream()) {
+				FileUtils.copy(inImage, new ShieldOutputStream(zout));
+			} catch(Exception e) {
+				log.error("", e);
+			}
+			zout.closeEntry();
+		}
+
+		RepositoryService repositoryService = CoreSpringFactory.getImpl(RepositoryService.class);
+		VFSLeaf movie = repositoryService.getIntroductionMovie(re);
+		if(movie != null) {
+			imp.setMovieName(movie.getName());
+			zout.putNextEntry(new ZipEntry(movie.getName()));
+			try(InputStream inMovie=movie.getInputStream()) {
+				FileUtils.copy(inMovie, new ShieldOutputStream(zout));
+			} catch(Exception e) {
+				log.error("", e);
+			}
+			zout.closeEntry();
+		}
+		
+		zout.putNextEntry(new ZipEntry(PROPERTIES_FILE));
+		getXStream().toXML(imp, new ShieldOutputStream(zout));
+		zout.closeEntry();
+	}
 
 	/**
 	 * Export a repository entry referenced by a course node to the given export directory.
diff --git a/src/main/java/org/olat/repository/handlers/RepositoryHandler.java b/src/main/java/org/olat/repository/handlers/RepositoryHandler.java
index 1d4427f91b4..e4c1779091c 100644
--- a/src/main/java/org/olat/repository/handlers/RepositoryHandler.java
+++ b/src/main/java/org/olat/repository/handlers/RepositoryHandler.java
@@ -106,7 +106,7 @@ public interface RepositoryHandler {
 	 * 
 	 * @param source
 	 * @param target
-	 * @return
+	 * @return The target repository entry
 	 */
 	public RepositoryEntry copy(Identity author, RepositoryEntry source, RepositoryEntry target);
 	
-- 
GitLab