/**
* 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.ims.qti.editor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.dom4j.Document;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.gui.translator.PackageTranslator;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.FileUtils;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.ZipUtil;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.NamedContainerImpl;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.xml.XMLParser;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.fileresource.FileResourceManager;
import org.olat.fileresource.types.FileResource;
import org.olat.ims.qti.QTIChangeLogMessage;
import org.olat.ims.qti.editor.beecom.objects.Assessment;
import org.olat.ims.qti.editor.beecom.objects.Metadata;
import org.olat.ims.qti.editor.beecom.objects.QTIDocument;
import org.olat.ims.qti.editor.beecom.objects.Section;
import org.olat.ims.qti.editor.beecom.parser.ParserManager;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.ImsRepositoryResolver;
import org.olat.ims.resources.IMSEntityResolver;
/**
*Initial Date: 27.08.2003
* @author Mike Stock
*/
public class QTIEditorPackage {
public static final String FOLDERNAMEFOR_CHANGELOG = "changelog";
/*
* Files are store in tmp directory as
* tmp/qtieditor/{login}/{repositoryEntryID}/ extracted from the repositoryEntry
*/
private static final String SERIALIZED_QTI_DOCUMENT = "__qti.xstream.xml";
private static final String CURRENT_HISTORY ="__qti.history.xml";
private Identity identity = null;
private FileResource fileResource = null;
private String packageSubDir = null;
private File packageDir = null;
private QTIDocument qtiDocument = null;
private boolean resumed = false;
private static OutputFormat outformat;
private OLog log = Tracing.createLoggerFor(this.getClass());
private Translator translator;
static {
outformat = OutputFormat.createPrettyPrint();
outformat.setEncoding("UTF-8");
}
/**
* @param identity
* @param fileResource
*/
public QTIEditorPackage(Identity identity, FileResource fileResource, Translator translator) {
this.identity = identity;
this.fileResource = fileResource;
this.translator = translator;
init();
}
/**
* Create a new qtipackage.
* @param title
* @param type
* @param locale
*/
public QTIEditorPackage(String title, String type, Locale locale) {
// create new qti document
translator = new PackageTranslator("org.olat.ims.qti", locale);
qtiDocument = new QTIDocument();
Assessment assessment = QTIEditHelper.createAssessment(title, type);
qtiDocument.setAssessment(assessment);
Section section = QTIEditHelper.createSection(translator);
List sectionList = new ArrayList();
sectionList.add(section);
assessment.setSections(sectionList);
List itemList = new ArrayList();
itemList.add(QTIEditHelper.createSCItem(translator));
section.setItems(itemList);
// initialize package
packageSubDir = CodeHelper.getGlobalForeverUniqueID();
packageDir = new File(getTmpBaseDir(), packageSubDir);
packageDir.mkdirs();
getMediaBaseDir().mkdirs();
getChangelogBaseDir().mkdirs();
}
private QTIEditorPackage() {
//
}
/**
* Return the underlying resourceable.
* @return OLATResourceable
*/
public OLATResourceable getRepresentingResourceable() {
return fileResource;
}
private void init() {
packageSubDir = getPackageSubDir(identity, fileResource);
packageDir = new File(getTmpBaseDir(), packageSubDir);
packageDir.mkdirs();
getMediaBaseDir().mkdirs();
getChangelogBaseDir().mkdirs();
}
/**
* Returns the sub directory within the base temp directory for this package.
* @param i
* @param fr
* @return Sub directory relative to temporary base directory.
*/
private String getPackageSubDir(Identity i, FileResource fr) {
return i.getName() + "/" + fr.getResourceableId();
}
/**
* Get the temporary root dir where all packages are located.
* @return The editor's package temp base directory.
*/
public static File getTmpBaseDir() {
return new File(WebappHelper.getUserDataRoot() + "/tmp/qtieditor/");
}
/**
* Return the media base URL for delivering media of this package.
* @return Complete media base URL.
*/
public String getMediaBaseURL() {
return WebappHelper.getServletContextPath() + "/secstatic/qtieditor/" + packageSubDir;
}
/**
* Returns the package's media directory.
* @return the media directory
*/
public File getMediaBaseDir() {
return new File(packageDir, "/media");
}
public VFSContainer getBaseDir() {
NamedContainerImpl namedBaseDir = new NamedContainerImpl(translator.translate("qti.basedir.displayname"), new LocalFolderImpl(packageDir));
return namedBaseDir;
}
/**
* Returns the package's change log directory
* @return change log directory
*/
public File getChangelogBaseDir(){
return new File(packageDir,"/"+FOLDERNAMEFOR_CHANGELOG);
}
/**
* Unzip package into temporary directory.
* @return true if successfull, false otherwise
*/
private boolean unzipPackage() {
FileResourceManager frm = FileResourceManager.getInstance();
File fPackageZIP = frm.getFileResource(fileResource);
return ZipUtil.unzip(fPackageZIP, packageDir);
}
/**
* @return Reutrns the QTIDocument structure
*/
public QTIDocument getQTIDocument() {
if (qtiDocument == null) {
if (hasSerializedQTIDocument()) {
qtiDocument = loadSerializedQTIDocument();
resumed = true;
} else {
unzipPackage();
Document doc = loadQTIDocument();
if(doc!=null) {
ParserManager parser = new ParserManager();
qtiDocument = (QTIDocument)parser.parse(doc);
// grab assessment type
Metadata meta = qtiDocument.getAssessment().getMetadata();
String assessType = meta.getField(AssessmentInstance.QMD_LABEL_TYPE);
if (assessType != null) {
qtiDocument.setSurvey(assessType.equals(AssessmentInstance.QMD_ENTRY_TYPE_SURVEY));
}
resumed = false;
} else {
qtiDocument = null;
}
}
}
return qtiDocument;
}
/**
* @return True upon success, false otherwise.
*/
public boolean savePackageToRepository() {
FileResourceManager frm = FileResourceManager.getInstance();
File tmpZipFile = new File(FolderConfig.getCanonicalTmpDir() + "/" + CodeHelper.getRAMUniqueID() + ".zip");
// first save complete ZIP package to repository
if (!savePackageTo(tmpZipFile)) return false;
// move file from temp to repository root and rename
File fRepositoryZip = frm.getFileResource(fileResource);
if (!FileUtils.moveFileToDir(tmpZipFile, frm.getFileResourceRoot(fileResource))) {
tmpZipFile.delete();
return false;
}
fRepositoryZip.delete();
new File(frm.getFileResourceRoot(fileResource), tmpZipFile.getName()).renameTo(fRepositoryZip);
// delete old unzip content. If the repository entry gets called in the meantime,
// the package will get unzipped again.
tmpZipFile.delete();
frm.deleteUnzipContent(fileResource);
// to be prepared for the next start, unzip right now.
return (frm.unzipFileResource(fileResource) != null);
}
/**
* save the change log in the changelog folder, must be called before savePackageToRepository.
* @param changelog
*/
public void commitChangelog(QTIChangeLogMessage clm) {
Date tmp = new Date(clm.getTimestmp());
java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss");
String filname = formatter.format(tmp);
filname += clm.isPublic() ? ".all" : ".group";
filname+=".txt";
File changelogFile = new File(getChangelogBaseDir(),filname);
FileUtils.save(changelogFile,clm.getLogMessage(),"utf-8");
}
/**
* Package the package to the given file.
* @param fOut
* @return True upon success.
*/
public boolean savePackageTo(File fOut) {
saveQTIDocument(qtiDocument.getDocument());
Set files = new HashSet(3);
files.add(ImsRepositoryResolver.QTI_FILE);
files.add("media");
files.add("changelog");
return ZipUtil.zip(files, packageDir, fOut);
}
/**
* Remove the media files specified in the input set (removable contains filenames)
* @param removable
*/
public void removeMediaFiles(Set<String> removable) {
LocalFolderImpl mediaFolder = new LocalFolderImpl(new File(packageDir,"media"));
List<VFSItem> allMedia = mediaFolder.getItems();
QTIEditHelper.removeUnusedMedia(removable, allMedia);
}
/**
* Saves a serialized versionof the underlying QTIDocument.
*
*/
public void serializeQTIDocument() {
XStreamHelper.writeObject(new File(packageDir, SERIALIZED_QTI_DOCUMENT), qtiDocument);
}
private boolean hasSerializedQTIDocument() {
return new File(packageDir, SERIALIZED_QTI_DOCUMENT).exists();
}
private QTIDocument loadSerializedQTIDocument() {
return (QTIDocument)XStreamHelper.readObject(new File(packageDir, SERIALIZED_QTI_DOCUMENT));
}
/**
* save a temporary file with the change history
* @param history
*/
public void serializeChangelog(Map history){
XStreamHelper.writeObject(new File(packageDir, CURRENT_HISTORY), history);
}
/**
* check if a serialized change log exists
* @return
*/
public boolean hasSerializedChangelog(){
return new File(packageDir, CURRENT_HISTORY).exists();
}
/**
* resume the change log from the temporary file
* @return
*/
public Map loadChangelog(){
return (Map)XStreamHelper.readObject(new File(packageDir, CURRENT_HISTORY));
}
/**
* Load a document from file.
*
* @return the loaded document or null if loading failed
*/
private Document loadQTIDocument() {
File fIn = null;
FileInputStream in = null;
BufferedInputStream bis = null;
Document doc = null;
try {
fIn = new File(packageDir, ImsRepositoryResolver.QTI_FILE);
in = new FileInputStream(fIn);
bis = new BufferedInputStream(in);
XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
doc = xmlParser.parse(bis, true);
} catch (Exception e) {
log.warn("Exception when parsing input QTI input stream for " + fIn != null ? fIn.getAbsolutePath() : "qti.xml", e);
return null;
} finally {
try {
if (in != null) in.close();
if (bis != null) bis.close();
} catch (Exception e) {
throw new OLATRuntimeException(this.getClass(), "Could not close input file stream ", e);
}
}
return doc;
}
/**
* SaveQTIDocument in temporary folder.
*
* @param doc
* @return true: save was successful, false otherwhise
*/
private boolean saveQTIDocument(Document doc) {
File fOut = null;
OutputStream out = null;
try {
fOut = new File(packageDir, ImsRepositoryResolver.QTI_FILE);
out = new FileOutputStream(fOut);
XMLWriter writer = new XMLWriter(out, outformat);
writer.write(doc);
writer.close();
} catch (Exception e) {
throw new OLATRuntimeException(this.getClass(), "Exception when saving QTI document to " + fOut != null ? fOut.getAbsolutePath() : "qti.xml", e);
} finally {
if (out != null) try {
out.close();
} catch (IOException e1) {
throw new OLATRuntimeException(this.getClass(), "Could not close output file stream ", e1);
}
}
return true;
}
/**
* Cleanup any temporary directory for this qti file only.
*/
public void cleanupTmpPackageDir() {
FileUtils.deleteDirsAndFiles(packageDir, true, true);
}
/**
* @return True if package has been resumed.
*/
public boolean isResumed() {
return resumed;
}
/**
* @param b
*/
public void setResumed(boolean b) {
resumed = b;
}
}