/**
* 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) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.modules.wiki;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import org.apache.commons.codec.binary.Base64;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.cache.n.CacheWrapper;
import org.olat.core.util.controller.OLATResourceableListeningWrapperController;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.SyncerExecutor;
import org.olat.core.util.notifications.SubscriptionContext;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.filters.VFSItemSuffixFilter;
import org.olat.core.util.vfs.filters.VFSLeafFilter;
import org.olat.course.CourseModule;
import org.olat.course.nodes.WikiCourseNode;
import org.olat.course.run.environment.CourseEnvironment;
import org.olat.fileresource.FileResourceManager;
import org.olat.fileresource.types.FileResource;
import org.olat.fileresource.types.WikiResource;
import org.olat.group.BusinessGroup;
import org.olat.modules.wiki.versioning.DifferenceService;
import org.olat.modules.wiki.versioning.diff.CookbookDifferenceService;
import org.olat.resource.OLATResource;
import org.olat.resource.OLATResourceManager;
/**
* Description:<br>
* This class handles several wiki's by storing them in a cache and also creates a new wikis.
* It handles also the file operation to persist the data in the wiki pages which are stored on the file
* system.
* <P>
* Initial Date: May 5, 2006 <br>
*
* @author guido
*/
public class WikiManager {
public static final String VIEW_COUNT = "view.count";
public static final String MODIFY_AUTHOR = "modify.author";
public static final String M_TIME = "mTime";
public static final String INITIAL_AUTHOR = "initial.author";
public static final String FORUM_KEY = "forum.key";
public static final String VERSION = "version";
public static final String C_TIME = "cTime";
public static final String PAGENAME = "pagename";
private static WikiManager instance = new WikiManager();
public static final String WIKI_RESOURCE_FOLDER_NAME = "wiki";
public static final String VERSION_FOLDER_NAME = "versions";
public static final String WIKI_FILE_SUFFIX = "wp";
public static final String WIKI_PROPERTIES_SUFFIX = "properties";
public static final String UPDATE_COMMENT = "update.comment";
OLog log = Tracing.createLoggerFor(this.getClass());
//o_clusterNOK cache : 08.04.08/cg Not tested in cluster-mode
CacheWrapper wikiCache;
/**
* return singleton
*/
public static WikiManager getInstance() {
return instance;
}
// ---- begin controller factory -----
/** @param ureq
* @param wControl
* @param ores either an OlatResourcable of an repository entry or of an BusinessGroup
* @param securityCallback a callback to evaluate the permissions
* @param initialPageName opens the wiki with an certain page, default is the index page if null is passed
* @param courseContext - a course context or null if used outside a course
* @param courseNodeContext - a courseNode context or null if used outside a course
* @param groupContext - a BusinessGroup or null if not used in group context
*/
public WikiMainController createWikiMainController(UserRequest ureq, WindowControl wControl, OLATResourceable ores, WikiSecurityCallback securityCallback, String initialPageName) {
return new WikiMainController(ureq, wControl, ores, securityCallback, initialPageName);
}
/**
* brasato:::: to discuss, are two methods needed at all? probably not, unless there are cases to launch this controller without an ores known.
* such as contentPackacking which requires only a fileroot (but this file directory depends on a ores in the end)
*
* create a wikiMaincontroller which disposes itself when the associated olatresourceable is disposed.
*/
public Controller createWikiMainControllerDisposeOnOres(UserRequest ureq, WindowControl wControl, OLATResourceable ores, WikiSecurityCallback securityCallback, String initialPageName) {
Controller controller = new WikiMainController(ureq, wControl, ores, securityCallback, initialPageName);
OLATResourceableListeningWrapperController dwc = new OLATResourceableListeningWrapperController(ureq, wControl, ores, controller, ureq.getIdentity());
return dwc;
}
// ---- end controller factory -----
/**
* @return the new created resource
*/
public FileResource createWiki() {
FileResource resource = new WikiResource();
createFolders(resource);
OLATResourceManager rm = OLATResourceManager.getInstance();
OLATResource ores = rm.createOLATResourceInstance(resource);
rm.saveOLATResource(ores);
return resource;
}
private void createFolders(OLATResourceable ores) {
long start = 0;
if (log.isDebug()) {
start = System.currentTimeMillis();
}
VFSContainer rootContainer = getWikiRootContainer(ores);
VFSContainer unzippedDir = (VFSContainer) rootContainer.resolve(FileResourceManager.ZIPDIR);
if (unzippedDir == null) { // check for _unzipped_ dir from imported wiki's
if (rootContainer.createChildContainer(WIKI_RESOURCE_FOLDER_NAME) == null) throwError(ores);
if (rootContainer.createChildContainer(WikiContainer.MEDIA_FOLDER_NAME) == null) throwError(ores);
if (rootContainer.createChildContainer(VERSION_FOLDER_NAME) == null) throwError(ores);
} else { // _unzipped_ dir found: move elements to wiki folder and delete
// unzipped dir and zip files
List files = unzippedDir.getItems();
VFSContainer wikiCtn = rootContainer.createChildContainer(WIKI_RESOURCE_FOLDER_NAME);
VFSContainer mediaCtn = rootContainer.createChildContainer(WikiContainer.MEDIA_FOLDER_NAME);
if (rootContainer.createChildContainer(VERSION_FOLDER_NAME) == null) throwError(ores);
if (wikiCtn == null) throwError(ores);
// copy files to wiki and media folder
for (Iterator iter = files.iterator(); iter.hasNext();) {
VFSLeaf leaf = ((VFSLeaf) iter.next());
if (leaf.getName().endsWith(WikiManager.WIKI_FILE_SUFFIX) || leaf.getName().endsWith(WikiManager.WIKI_PROPERTIES_SUFFIX)) {
wikiCtn.copyFrom(leaf);
} else {
if (leaf.getName().contains(WikiManager.WIKI_FILE_SUFFIX+"-") || leaf.getName().contains(WikiManager.WIKI_PROPERTIES_SUFFIX+"-")) {
leaf.delete(); // delete version history
} else
mediaCtn.copyFrom(leaf);
}
}
unzippedDir.delete();
List zipFiles = rootContainer.getItems(new VFSItemSuffixFilter(new String[] { "zip" }));
// delete all zips
for (Iterator iter = zipFiles.iterator(); iter.hasNext();) {
VFSLeaf element = (VFSLeaf) iter.next();
element.delete();
}
//reset forum key and author references keys back to default as users and forums may not exist
List propertyLeafs = wikiCtn.getItems(new VFSItemSuffixFilter(new String[] { WikiManager.WIKI_PROPERTIES_SUFFIX }));
for (Iterator iter = propertyLeafs.iterator(); iter.hasNext();) {
VFSLeaf element = (VFSLeaf) iter.next();
WikiPage page = Wiki.assignPropertiesToPage(element);
page.setForumKey(0);
page.setInitalAuthor(0);
page.setModifyAuthor(0);
page.setModificationTime(0);
page.setViewCount(0);
page.setVersion("0");
page.setCreationTime(System.currentTimeMillis());
saveWikiPageProperties(ores, page);
}
}
if (log.isDebug()) {
long end = System.currentTimeMillis();
log.debug("creating folders and move files and updating properties to default values took: (milliseconds)"+(end-start));
}
}
private void throwError(OLATResourceable ores) {
throw new OLATRuntimeException(this.getClass(), "Unable to create wiki folder structure for resource: " + ores.getResourceableId(),
null);
}
/**
* @param ores
* @return a wiki loaded from cache or the fileSystem
*/
public Wiki getOrLoadWiki(final OLATResourceable ores) {
final String wikiKey = OresHelper.createStringRepresenting(ores);
//cluster_OK by guido
if (wikiCache == null) {
CoordinatorManager.getCoordinator().getSyncer().doInSync(ores, new SyncerExecutor() {
public void execute() {
if (wikiCache == null) {
wikiCache = CoordinatorManager.getCoordinator().getCacher().getOrCreateCache(this.getClass(), "wiki");
}
}
});
}
Wiki wiki = (Wiki) wikiCache.get(wikiKey);
if (wiki != null) {
log.debug("loading wiki from cache. Ores: " + ores.getResourceableId());
return wiki;
}
// No wiki in cache => load it form file-system
CoordinatorManager.getCoordinator().getSyncer().doInSync(ores, new SyncerExecutor() {
public void execute() {
long start = 0;
// wiki not in cache load form filesystem
if (log.isDebug()) {
log.debug("wiki not in cache. Loading wiki from filesystem. Ores: " + ores.getResourceableId());
start = System.currentTimeMillis();
}
Wiki wiki = null;
VFSContainer folder = getWikiContainer(ores, WIKI_RESOURCE_FOLDER_NAME);
// wiki folder structure does not yet exists, but resource does. Create
// wiki in group context
if (folder == null) {
// createWikiforExistingResource(ores);
createFolders(ores);
folder = getWikiContainer(ores, WIKI_RESOURCE_FOLDER_NAME);
}
// folders should be present, create the wiki
wiki = new Wiki(getWikiRootContainer(ores));
// filter for xyz.properties files
List wikiLeaves = folder.getItems(new VFSItemSuffixFilter(new String[] { WikiManager.WIKI_PROPERTIES_SUFFIX }));
for (Iterator iter = wikiLeaves.iterator(); iter.hasNext();) {
VFSLeaf propertiesFile = (VFSLeaf) iter.next();
WikiPage page = Wiki.assignPropertiesToPage(propertiesFile);
if (page == null) {
// broken pages get automatically cleaned from filesystem
String contentFileToBeDeleted = (propertiesFile.getName().substring(0,
propertiesFile.getName().length() - WikiManager.WIKI_PROPERTIES_SUFFIX.length()) + WikiManager.WIKI_FILE_SUFFIX);
folder.resolve(contentFileToBeDeleted).delete();
propertiesFile.delete();
continue;
}
// index and menu page are loaded by default
if (page.getPageName().equals(WikiPage.WIKI_INDEX_PAGE) || page.getPageName().equals(WikiPage.WIKI_MENU_PAGE)) {
VFSLeaf leaf = (VFSLeaf) folder.resolve(page.getPageId() + "." + WikiManager.WIKI_FILE_SUFFIX);
page.setContent(FileUtils.load(leaf.getInputStream(), "utf-8"));
}
// due to a bug we have to rename some pages that start with an non
// ASCII lowercase letter
String idOutOfFileName = propertiesFile.getName().substring(0, propertiesFile.getName().indexOf("."));
if (!page.getPageId().equals(idOutOfFileName)) {
// rename corrupt prop file
propertiesFile.rename(page.getPageId() + "." + WikiManager.WIKI_PROPERTIES_SUFFIX);
// load content and delete corrupt content file
VFSLeaf contentFile = (VFSLeaf) folder.resolve(idOutOfFileName + "." + WikiManager.WIKI_FILE_SUFFIX);
contentFile.rename(page.getPageId() + "." + WikiManager.WIKI_FILE_SUFFIX);
}
wiki.addPage(page);
}
// if index and menu page not present create the first page and save it
if (wiki.getNumberOfPages() == 0) {
WikiPage indexPage = new WikiPage(WikiPage.WIKI_INDEX_PAGE);
WikiPage menuPage = new WikiPage(WikiPage.WIKI_MENU_PAGE);
indexPage.setCreationTime(System.currentTimeMillis());
wiki.addPage(indexPage);
menuPage.setCreationTime(System.currentTimeMillis());
menuPage.setContent("* [[Index]]\n* [[Index|Your link]]");
wiki.addPage(menuPage);
saveWikiPage(ores, indexPage, false, wiki);
saveWikiPage(ores, menuPage, false, wiki);
}
// add pages internally used for displaying dynamic data, they are not
// persisted
WikiPage recentChangesPage = new WikiPage(WikiPage.WIKI_RECENT_CHANGES_PAGE);
WikiPage a2zPage = new WikiPage(WikiPage.WIKI_A2Z_PAGE);
wiki.addPage(recentChangesPage);
wiki.addPage(a2zPage);
// wikiCache.put(OresHelper.createStringRepresenting(ores), wiki);
if (log.isDebug()) {
long stop = System.currentTimeMillis();
log.debug("loading of wiki from filessystem took (ms) " + (stop - start));
}
wikiCache.put(wikiKey, wiki);
}
});
//at this point there will be something in the cache
return (Wiki) wikiCache.get(wikiKey);
}
public DifferenceService getDiffService() {
return new CookbookDifferenceService();
}
/**
* persists a wiki page on the filesystem. It moves the recent page and the
* metadata to the versions folder with the version on the tail and saves new
* page with metadata to the wiki folder. Does not need to be synchronized as
* editing is locked on page level by the
*
* @see WikiMainController
* @param ores
* @param page
*/
protected void saveWikiPage(OLATResourceable ores, WikiPage page, boolean incrementVersion, Wiki wiki) {
//cluster_OK by guido
VFSContainer versionsContainer = getWikiContainer(ores, VERSION_FOLDER_NAME);
VFSContainer wikiContentContainer = getWikiContainer(ores, WIKI_RESOURCE_FOLDER_NAME);
// rename existing content file to version x and copy it to the version
// container
VFSItem item = wikiContentContainer.resolve(page.getPageId() + "." + WIKI_FILE_SUFFIX);
if (item != null && incrementVersion) {
if (page.getVersion() > 0) {
versionsContainer.copyFrom(item);
VFSItem copiedItem = versionsContainer.resolve(page.getPageId() + "." + WIKI_FILE_SUFFIX);
String fileName = page.getPageId() + "." + WIKI_FILE_SUFFIX + "-" + page.getVersion();
copiedItem.rename(fileName);
}
item.delete();
}
// rename existing meta file to version x and copy it to the version
// container
item = wikiContentContainer.resolve(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
if (item != null && incrementVersion) {
// TODO renaming and coping does not work. Bug?? felix fragen
if (page.getVersion() > 0) {
versionsContainer.copyFrom(item);
VFSItem copiedItem = versionsContainer.resolve(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
String fileName = page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX + "-" + page.getVersion();
copiedItem.rename(fileName);
}
item.delete();
}
// store recent content file
VFSLeaf leaf = wikiContentContainer.createChildLeaf(page.getPageId() + "." + WIKI_FILE_SUFFIX);
if(leaf == null) throw new AssertException("Tried to save wiki page with id ("+page.getPageId()+") and Olatresource: "+ores.getResourceableId()+" but page already existed!");
FileUtils.save(leaf.getOutputStream(false), page.getContent(), "utf-8");
// store recent properties file
leaf = wikiContentContainer.createChildLeaf(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
if (leaf == null) throw new AssertException("could not create file for wiki page "+page.getPageId()+", ores: "+ores.getResourceableTypeName()+":"+ores.getResourceableId()+", wikicontainer:"+wikiContentContainer);
if (incrementVersion) page.incrementVersion();
// update modification time
if (!page.getContent().equals("")) page.setModificationTime(System.currentTimeMillis());
Properties p = getPageProperties(page);
try {
OutputStream os = leaf.getOutputStream(false);
p.store(os, "wiki page meta properties");
os.close();
// if (incrementVersion) page.incrementVersion();
} catch (IOException e) {
throw new OLATRuntimeException(WikiManager.class, "failed to save wiki page properties for page with id: " + page.getPageId() + " and olatresource: " + ores.getResourceableId(), e);
}
page.setViewCount(0); //reset view count of the page
//update cache to inform all nodes about the change
if (wikiCache!=null) {
wikiCache.update(OresHelper.createStringRepresenting(ores), wiki);
}
}
/**
* delete a page completely by removing from the file system
*
* @param ores
* @param page
*/
protected void deleteWikiPage(OLATResourceable ores, WikiPage page) {
String name = page.getPageName();
// do not delete default pages
if (name.equals(WikiPage.WIKI_INDEX_PAGE) || name.equals(WikiPage.WIKI_MENU_PAGE)) return;
VFSContainer wikiContentContainer = getWikiContainer(ores, WIKI_RESOURCE_FOLDER_NAME);
VFSContainer versionsContainer = getWikiContainer(ores, VERSION_FOLDER_NAME);
//delete content and property file
VFSItem item = wikiContentContainer.resolve(page.getPageId() + "." + WIKI_FILE_SUFFIX);
if (item != null) item.delete();
item = wikiContentContainer.resolve(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
if (item != null) item.delete();
//delete all version files of the page
List leafs = versionsContainer.getItems(new VFSLeafFilter());
if (leafs.size() > 0) {
for (Iterator iter = leafs.iterator(); iter.hasNext();) {
VFSLeaf leaf = (VFSLeaf) iter.next();
String filename = leaf.getName();
if (filename.startsWith(page.getPageId())) {
leaf.delete();
}
}
}
log.audit("Deleted wiki page with name: " + page.getPageName() + " from resourcable id: "+ ores.getResourceableId());
if (wikiCache!=null) {
wikiCache.update(OresHelper.createStringRepresenting(ores), getOrLoadWiki(ores));
}
}
/**
* delete a whole wiki from the cache and the filesystem
* @param ores
*/
protected void deleteWiki(OLATResourceable ores) {
if (wikiCache!=null) {
wikiCache.remove(OresHelper.createStringRepresenting(ores));
}
OLATResourceManager.getInstance().deleteOLATResourceable(ores);
}
/**
* @param ores
* @param page
*/
public void updateWikiPageProperties(OLATResourceable ores, WikiPage page) {
saveWikiPageProperties(ores, page);
if (wikiCache!=null) {
wikiCache.update(OresHelper.createStringRepresenting(ores), getOrLoadWiki(ores));
}
}
private void saveWikiPageProperties(OLATResourceable ores, WikiPage page) {
VFSContainer wikiContentContainer = getWikiContainer(ores, WIKI_RESOURCE_FOLDER_NAME);
VFSLeaf leaf = (VFSLeaf) wikiContentContainer.resolve(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
if (leaf == null) leaf = wikiContentContainer.createChildLeaf(page.getPageId() + "." + WIKI_PROPERTIES_SUFFIX);
Properties p = getPageProperties(page);
try {
p.store(leaf.getOutputStream(false), "wiki page meta properties");
} catch (IOException e) {
throw new OLATRuntimeException(WikiManager.class, "failed to save wiki page properties for page with id: " + page.getPageId() +" and olatresource: " + ores.getResourceableId(), e);
}
}
/**
* @param page
* @return the fields of the page object as properties
*/
private Properties getPageProperties(WikiPage page) {
Properties p = new Properties();
p.setProperty(PAGENAME, page.getPageName());
p.setProperty(VERSION, String.valueOf(page.getVersion()));
p.setProperty(FORUM_KEY, String.valueOf(page.getForumKey()));
p.setProperty(INITIAL_AUTHOR, String.valueOf(page.getInitalAuthor()));
p.setProperty(MODIFY_AUTHOR, String.valueOf(page.getModifyAuthor()));
p.setProperty(C_TIME, String.valueOf(page.getCreationTime()));
p.setProperty(VIEW_COUNT, String.valueOf(page.getViewCount()));
p.setProperty(M_TIME, String.valueOf(page.getModificationTime()));
p.setProperty(UPDATE_COMMENT, page.getUpdateComment());
return p;
}
/**
* @param pageName
* @return
*/
public static String generatePageId(String pageName) {
try {
String encoded = new String(Base64.encodeBase64(pageName.getBytes("utf-8")), "us-ascii");
encoded = encoded.replace('/', '_'); //base64 can contain "/" so we have to replace them
return encoded;
} catch (UnsupportedEncodingException e) {
throw new OLATRuntimeException(WikiManager.class, "Encoding UTF-8 not supported by your platform!", e);
}
}
/**
* @param ores
* @param folderName
* @return the Vfs container or null if not found
*/
public VFSContainer getWikiContainer(OLATResourceable ores, String folderName) {
VFSContainer wikiRootContainer = getWikiRootContainer(ores);
return (VFSContainer) wikiRootContainer.resolve(folderName);
}
/**
* Returns the root-container for certain OLAT-resourceable.
* @param ores
* @return
*/
public VFSContainer getWikiRootContainer(OLATResourceable ores) {
// Check if Resource is a BusinessGroup, because BusinessGroup-wiki's are stored at a different place
if(log.isDebug()){
log.debug("calculating wiki root container with ores id: "+ores.getResourceableId()+" and resourcable type name: "+ores.getResourceableTypeName());
}
if (isGroupContextWiki(ores)) {
// Group Wiki
return new OlatRootFolderImpl(getGroupWikiRelPath(ores), null);
} else {
// Repository Wiki
return FileResourceManager.getInstance().getFileResourceRootImpl(ores);
}
}
/**
* Get Wiki-File-Path for certain BusinessGroup.
* @param businessGroup
* @return
*/
private String getGroupWikiRelPath(OLATResourceable ores) {
return "/cts/wikis/" + ores.getResourceableTypeName() + "/" + ores.getResourceableId();
}
/**
* Return Media folder for uploading files.
* @param ores
* @return
*/
public OlatRootFolderImpl getMediaFolder(OLATResourceable ores) {
// Check if Resource is a BusinessGroup, because BusinessGroup-wiki's are stored at a different place
if (isGroupContextWiki(ores)) {
// Group Wiki
return new OlatRootFolderImpl(getGroupWikiRelPath(ores) + "/" + WikiContainer.MEDIA_FOLDER_NAME, null);
} else {
// Repository Wiki
return new OlatRootFolderImpl("/repository/" + ores.getResourceableId() + "/" + WikiContainer.MEDIA_FOLDER_NAME, null);
}
}
/**
* @return false if repository wiki or true if group only wiki
*/
/**
* @return false if repository wiki or true if group only wiki
*/
protected boolean isGroupContextWiki(OLATResourceable ores) {
return ores.getResourceableTypeName().equals(OresHelper.calculateTypeName(BusinessGroup.class));
}
/**
* wiki subscription context for wikis in the course.
* @param cenv
* @param wikiCourseNode
* @return
*/
public static SubscriptionContext createTechnicalSubscriptionContextForCourse(CourseEnvironment cenv, WikiCourseNode wikiCourseNode) {
return new SubscriptionContext(CourseModule.getCourseTypeName(), cenv.getCourseResourceableId(), wikiCourseNode.getIdent());
}
}