/**
* 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.iq;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.dom4j.Document;
import org.hibernate.Hibernate;
import org.hibernate.type.Type;
import org.olat.admin.user.delete.service.UserDeletionManager;
import org.olat.basesecurity.Manager;
import org.olat.basesecurity.ManagerFactory;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.layout.GenericMainController;
import org.olat.core.gui.control.generic.layout.MainLayoutController;
import org.olat.core.gui.control.generic.messages.MessageController;
import org.olat.core.gui.control.generic.messages.MessageUIFactory;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.OlatResourceableType;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.Util;
import org.olat.core.util.controller.OLATResourceableListeningWrapperController;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.LockResult;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.VFSConstants;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.VFSStatus;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.ims.qti.QTIResult;
import org.olat.ims.qti.QTIResultSet;
import org.olat.ims.qti.container.AssessmentContext;
import org.olat.ims.qti.container.HttpItemInput;
import org.olat.ims.qti.container.ItemContext;
import org.olat.ims.qti.container.ItemInput;
import org.olat.ims.qti.container.ItemsInput;
import org.olat.ims.qti.container.SectionContext;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.FilePersister;
import org.olat.ims.qti.process.Resolver;
import org.olat.ims.qti.render.LocalizedXSLTransformer;
import org.olat.ims.qti.render.ResultsBuilder;
import org.olat.modules.ModuleConfiguration;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryManager;
import org.olat.user.UserDataDeletable;
import org.olat.util.logging.activity.LoggingResourceable;
/**
* Initial Date: Mar 4, 2004
* @author Mike Stock
*/
public class IQManager implements UserDataDeletable {
private static final IQManager INSTANCE = new IQManager();
/**
*
*/
private IQManager() {
UserDeletionManager.getInstance().registerDeletableUserData(this);
}
/**
* @return Singleton.
*/
public static IQManager getInstance() {
return INSTANCE;
}
//--- methods for controller creation
/**
* IMS QTI Display Controller from within course -> moduleConfiguration
*
* concurrent access check needed -> Editor may save (commit changes) while displaying reads old/new data mix (files and xml structure)
*
*/
public Controller createIQDisplayController(ModuleConfiguration moduleConfiguration, IQSecurityCallback secCallback, UserRequest ureq,
WindowControl wControl, long callingResId, String callingResDetail) {
//two cases:
// -- VERY RARE CASE -- 1) qti is open in an editor session right now on the screen (or session on the way to timeout)
// -- 99% of cases -- 2) qti is ready to be run as test/survey
String repositorySoftkey = (String) moduleConfiguration.get(IQEditController.CONFIG_KEY_REPOSITORY_SOFTKEY);
RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntryBySoftkey(repositorySoftkey, true);
if (CoordinatorManager.getCoordinator().getLocker().isLocked(re.getOlatResource(), null)){
Translator translator = Util.createPackageTranslator(this.getClass(), ureq.getLocale());
//so this resource is locked, let's find out who locked it
LockResult lockResult = CoordinatorManager.getCoordinator().getLocker().acquireLock(re.getOlatResource(), ureq.getIdentity(), null);
return MessageUIFactory.createInfoMessage(ureq, wControl, translator.translate("status.currently.locked.title"),
translator.translate("status.currently.locked", new String[] {lockResult.getOwner().getName()}));
}else{
ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(re, OlatResourceableType.iq));
return new IQDisplayController(moduleConfiguration, secCallback, ureq, wControl, callingResId, callingResDetail);
}
}
/**
* IMS QTI Display Controller used by QTI Editor for preview.
*
* no concurrency protection needed here -> it is Editor <-> Preview of edited file
*
* @param resolver
* @param type
* @param secCallback
* @param ureq
* @param wControl
*/
public IQDisplayController createIQDisplayController(Resolver resolver, String type, IQSecurityCallback secCallback,
UserRequest ureq, WindowControl wControl) {
return new IQDisplayController(resolver, type, secCallback, ureq, wControl);
}
/**
* IMS QTI Display Controller used for IMS course node run view, or for the direct launching from learning resources.
*
* concurrent access check needed -> Editor may save (commit changes) while displaying reads old/new data mix (files and xml structure)
*
*
* @param res
* @param resolver
* @param type
* @param secCallback
* @param ureq
* @param wControl
* @return
*/
public MainLayoutController createIQDisplayController(OLATResourceable res, Resolver resolver, String type, IQSecurityCallback secCallback,
UserRequest ureq, WindowControl wControl) {
ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(res, OlatResourceableType.iq));
//two cases:
// -- VERY RARE CASE -- 1) qti is open in an editor session right now on the screen (or session on the way to timeout)
// -- 99% of cases -- 2) qti is ready to be run as test/survey
if (CoordinatorManager.getCoordinator().getLocker().isLocked(res, null)){
GenericMainController glc = createLockedMessageController(ureq, wControl);
glc.init(ureq);
return glc;
}else{
Controller controller = new IQDisplayController(resolver, type, secCallback, ureq, wControl);
OLATResourceableListeningWrapperController dwc = new OLATResourceableListeningWrapperController(ureq, wControl, res, controller, ureq.getIdentity());
return dwc;
}
}
private GenericMainController createLockedMessageController(UserRequest ureq, WindowControl wControl) {
//
//wrap simple message into mainLayout
GenericMainController glc = new GenericMainController(ureq, wControl) {
private MessageController contentCtr;
private Panel empty;
private LayoutMain3ColsController columnLayoutCtr;
@Override
public void init(UserRequest ureq) {
empty = new Panel("empty"); Translator translator = Util.createPackageTranslator(this.getClass(), ureq.getLocale());
contentCtr = MessageUIFactory.createInfoMessage(ureq, getWindowControl(), translator.translate("status.currently.locked.title"), translator.translate("status.currently.locked"));
listenTo(contentCtr); // auto dispose later
Component resComp = contentCtr.getInitialComponent();
columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), empty, empty, resComp, /*do not save no prefs*/null);
listenTo(columnLayoutCtr); // auto dispose later
putInitialPanel(columnLayoutCtr.getInitialComponent());
}
@Override
protected Controller handleOwnMenuTreeEvent(Object uobject, UserRequest ureq) {
//no menutree means no menu events.
return null;
}
};
return glc;
}
// --- end of controller creation
/**
*
* @param ai
* @param ureq
* @return
*/
public Document getResultsReporting(AssessmentInstance ai, UserRequest ureq) {
ResultsBuilder resB = new ResultsBuilder();
return resB.getResDoc(ai, ureq.getLocale(), ureq.getIdentity());
}
/**
*
* @param identity
* @param type
* @param assessID
* @return
*/
public Document getResultsReportingFromFile(Identity identity, String type, long assessID) {
return FilePersister.retreiveResultsReporting(identity, type, assessID);
}
/**
*
* @param docResReporting
* @param locale
* @param detailed
* @return
*/
public String transformResultsReporting(Document docResReporting, Locale locale, int summaryType) {
switch (summaryType) {
case AssessmentInstance.SUMMARY_COMPACT: // Result summary without solutions
ResultsBuilder.stripDetails(docResReporting);
break;
case AssessmentInstance.SUMMARY_SECTION: // Section summary without solutions
ResultsBuilder.stripItemResults(docResReporting);
System.out.println("TEST: Section summary without solutions");
break;
case AssessmentInstance.SUMMARY_DETAILED:// Strip nothing
break;
default: // default => Strip nothing
break;
}
StringBuilder sb = LocalizedXSLTransformer.getInstance(locale).renderResults(docResReporting);
return sb.toString();
}
/**
* Extract item inputs from http request
*
* @param ureq The request to extract item responses from.
* @return ItemsInput
*/
public ItemsInput getItemsInput(UserRequest ureq) {
ItemsInput result = new ItemsInput();
Enumeration params = ureq.getHttpReq().getParameterNames();
while (params.hasMoreElements()) {
String paramKey = (String) params.nextElement();
StringTokenizer st = new StringTokenizer(paramKey, "§", false);
String value = ureq.getParameter(paramKey);
if (st.countTokens() == 4) {
String itemType = st.nextToken();
String itemIdent = st.nextToken();
String responseID = st.nextToken();
HttpItemInput itemInput = (HttpItemInput) result.getItemInput(itemIdent);
if (itemInput == null) {
itemInput = new HttpItemInput(itemIdent);
result.addItemInput(itemInput);
}
// 'dummy' type is used to make sure iteminput is constructed for
// all items. it does not provide any response data
if (itemType.equals("qti")) itemInput.putSingle(responseID, value);
}
// refactoring to new setFormDirty() javascript method sends now an additional param "olat_fosm" which has no tokens inside
// so assertExc. is useless.
//else {
// throw new AssertException ("not 4 tokens in form name: orig='"+paramKey+"'");
//}
//<input id="QTI_1098869464495" type="checkbox"
// name="qti§QTIEDIT:MCQ:1098869464490§1098869464492§1098869464495" ....
}
return result;
}
/**
* Create the QTIResults on the database for a given assessments,
* self-assessment or survey. These database entries can be used for
* statistical downloads.
*
*
* @param ai
* @param resId
* @param resDetail
* @param ureq
*/
public void persistResults(AssessmentInstance ai, long resId, String resDetail, UserRequest ureq) {
AssessmentContext ac = ai.getAssessmentContext();
QTIResultSet qtiResultSet = new QTIResultSet();
qtiResultSet.setLastModified(new Date(System.currentTimeMillis()));
qtiResultSet.setOlatResource(resId);
qtiResultSet.setOlatResourceDetail(resDetail);
qtiResultSet.setRepositoryRef(ai.getRepositoryEntryKey());
qtiResultSet.setIdentity(ureq.getIdentity());
qtiResultSet.setQtiType(ai.getType());
qtiResultSet.setAssessmentID(ai.getAssessID());
qtiResultSet.setDuration(new Long(ai.getAssessmentContext().getDuration()));
// TODO qtiResultSet.setLastModified();
if (ai.isSurvey()){
qtiResultSet.setScore(0);
qtiResultSet.setIsPassed(true);
} else {
qtiResultSet.setScore(ac.getScore());
qtiResultSet.setIsPassed(ac.isPassed());
}
DBFactory.getInstance().saveObject(qtiResultSet);
// Loop over all sections in this assessment
int sccnt = ac.getSectionContextCount();
for (int i = 0; i < sccnt; i++) {
// Loop over all items in this section
SectionContext sc = ac.getSectionContext(i);
int iccnt = sc.getItemContextCount();
for (int j = 0; j < iccnt; j++) {
ItemContext ic = sc.getItemContext(j);
// Create new result item for this item
QTIResult qtiResult = new QTIResult();
qtiResult.setResultSet(qtiResultSet);
qtiResult.setItemIdent(ic.getIdent());
qtiResult.setDuration(new Long(ic.getTimeSpent()));
if (ai.isSurvey()) qtiResult.setScore(0);
else qtiResult.setScore(ic.getScore());
qtiResult.setTstamp(new Date(ic.getLatestAnswerTime()));
qtiResult.setLastModified(new Date(System.currentTimeMillis()));
qtiResult.setIp(ureq.getHttpReq().getRemoteAddr());
// Get user answers for this item
StringBuilder sb = new StringBuilder();
if (ic.getItemInput() == null) {} else {
ItemInput inp = ic.getItemInput();
if (inp.isEmpty()) {
sb.append("[]");
} else {
Map im = inp.getInputMap();
// Create answer block
Set keys = im.keySet();
Iterator iter = keys.iterator();
while (iter.hasNext()) {
String ident = (String) iter.next();
sb.append(ident); // response_lid ident
sb.append("[");
List answers = inp.getAsList(ident);
for (int y = 0; y < answers.size(); y++) {
sb.append("[");
String answer = (String) answers.get(y);
// answer is referenced to response_label ident, if
// render_choice
// answer is userinput, if render_fib
answer = quoteSpecialQTIResultCharacters(answer);
sb.append(answer);
sb.append("]");
}
sb.append("]");
}
}
}
qtiResult.setAnswer(sb.toString());
// Persist result data in database
DBFactory.getInstance().saveObject(qtiResult);
}
}
}
/**
*
* @param assessmentID
* @return
*/
public List findQtiResults(long assessmentID) {
DB persister = DBFactory.getInstance();
return persister.find("from q in class org.olat.ims.qti.QTIResult where q.assessmentID = ?",
new Long(assessmentID), Hibernate.LONG);
}
/**
*
* @param assessmentID
* @param versionID
* @return
*/
public List findQtiResults(long assessmentID, long versionID) {
DB persister = DBFactory.getInstance();
return persister.find("from q in class org.olat.ims.qti.QTIResult where "
+ "q.assessmentID = ? and q.versionid = ?", new Object[]{new Long(assessmentID), new Long(versionID)},
new Type[]{Hibernate.LONG, Hibernate.LONG});
}
/**
*
* @param assessmentID
* @param versionID
* @param itemIdent
* @return
*/
public List findQtiResults(long assessmentID, long versionID, String itemIdent) {
DB persister = DBFactory.getInstance();
return persister.find("from q in class org.olat.ims.qti.QTIResult where "
+ "q.assessmentID = ? and q.versionid = ? and q.itemident = ?", new Object[]{new Long(assessmentID),
new Long(versionID), itemIdent}, new Type[]{Hibernate.LONG, Hibernate.LONG, Hibernate.STRING});
}
/**
*
* @param identity
* @param assessmentID
* @return
*/
//TODO: chg: No References to this method, QTIResult has no identity attribute => Query does NOT work !!! => Remove this code
// public List findQtiResults(Identity identity, long assessmentID) {
// DB persister = DBFactory.getInstance();
// return persister.find("from q in class org.olat.ims.qti.QTIResult where " + "q.assessmentID = ? and q.identity = ?",
// new Object[]{new Long(assessmentID), identity.getKey()}, new Type[]{Hibernate.LONG, Hibernate.LONG});
// }
/**
*
* @param identity
* @param assessmentID
* @param versionID
* @return
*/
// TODO: chg: No References to this method, QTIResult has no identity attribute => Query does NOT work !!! => Remove this code
// public List findQtiResults(Identity identity, long assessmentID, long versionID) {
// DB persister = DBFactory.getInstance();
// return persister.find("from q in class org.olat.ims.qti.QTIResult where "
// + "q.assessmentID = ? and q.versionid = ? and q.identity = ?", new Object[]{new Long(assessmentID),
// new Long(versionID), identity.getKey()}, new Type[]{Hibernate.LONG, Hibernate.LONG, Hibernate.LONG});
// }
/**
*
* @param identity
* @param assessmentID
* @param versionID
* @param itemIdent
* @return
*/
//TODO: chg: No References to this method, QTIResult has no identity attribute => Query does NOT work !!! => Remove this code
// public List findQtiResults(Identity identity, long assessmentID, long versionID, String itemIdent) {
// DB persister = DBFactory.getInstance();
// return persister.find("from q in class org.olat.ims.qti.QTIResult where "
// + "q.assessmentID = ? and q.versionid = ? and q.identity = ? and q.itemident = ?", new Object[]{
// new Long(assessmentID), new Long(versionID), identity.getKey(), itemIdent}, new Type[]{Hibernate.LONG,
// Hibernate.LONG, Hibernate.LONG, Hibernate.STRING});
// }
/**
* Qotes special characters used by the QTIResult answer formatting. Special
* characters are '\', '[', ']', '\t', '\n', '\r', '\f', '\a' and '\e'
*
* @param string The string to be quoted
* @return The quoted string
*/
public String quoteSpecialQTIResultCharacters(String string) {
string = string.replaceAll("\\\\", "\\\\\\\\");
string = string.replaceAll("\\[", "\\\\[");
string = string.replaceAll("\\]", "\\\\]");
string = string.replaceAll("\\t", "\\\\t");
string = string.replaceAll("\\n", "\\\\n");
string = string.replaceAll("\\r", "\\\\r");
string = string.replaceAll("\\f", "\\\\f");
string = string.replaceAll("\\a", "\\\\a");
string = string.replaceAll("\\e", "\\\\e");
return string;
}
/**
* Unquotes special characters in the QTIResult answer texts.
*
* @see org.olat.modules.iq.IQManager#quoteSpecialQTIResultCharacters(String)
* @param string
* @return The unquoted sting
*/
public String unQuoteSpecialQTIResultCharacters(String string) {
string = string.replaceAll("\\\\[", "\\[");
string = string.replaceAll("\\\\]", "\\]");
string = string.replaceAll("\\\\t", "\\t");
string = string.replaceAll("\\\\n", "\\n");
string = string.replaceAll("\\\\r", "\\r");
string = string.replaceAll("\\\\f", "\\f");
string = string.replaceAll("\\\\a", "\\a");
string = string.replaceAll("\\\\e", "\\e");
string = string.replaceAll("\\\\\\\\", "\\\\");
return string;
}
/**
* Delete all qti.ser and qti-resreporting files.
* @see org.olat.user.UserDataDeletable#deleteUserData(org.olat.core.id.Identity)
*/
public void deleteUserData(Identity identity, String newDeletedUserName) {
FilePersister.deleteUserData(identity);
Tracing.logDebug("Delete all qti.ser data and qti-resreporting data for identity=" + identity, this.getClass());
}
/**
* Returns null if no QTIResultSet found.
* @param identity
* @param olatResource
* @param olatResourceDetail
* @return Returns the last recorded QTIResultSet
*/
public QTIResultSet getLastResultSet(Identity identity, long olatResource, String olatResourceDetail) {
QTIResultSet returnQTIResultSet=null;
DB persister = DBFactory.getInstance();
List resultSetList = persister.find("from q in class org.olat.ims.qti.QTIResultSet where "
+ "q.identity = ? and q.olatResource = ? and q.olatResourceDetail = ? order by q.creationDate desc", new Object[]{identity.getKey(), new Long(olatResource), new String(olatResourceDetail)},
new Type[]{Hibernate.LONG, Hibernate.LONG, Hibernate.STRING});
Iterator resultSetIterator = resultSetList.iterator();
while(resultSetIterator.hasNext()) {
returnQTIResultSet = (QTIResultSet)resultSetIterator.next();
break;
}
return returnQTIResultSet;
}
/**
* This should only be used as fallback solution if the assessmentID is not available via the AssessmentManager
* (migration of old tests)
* @param identity
* @param olatResource is the course id
* @param olatResourceDetail is the node id
* @return Returns the last assessmentID if at least a QTIResultSet was stored for the input variables, null otherwise.
*/
public Long getLastAssessmentID(Identity identity, long olatResource, String olatResourceDetail) {
QTIResultSet resultSet = getLastResultSet(identity, olatResource, olatResourceDetail);
if(resultSet!=null) {
return resultSet.getAssessmentID();
}
return null;
}
/**
* Get identities with exists qti.ser file.
* @param resourceableId
* @param ident
* @return
*/
public List<Identity> getIdentitiesWithQtiSerEntry(Long resourceableId, String ident) {
List<Identity> identities = new ArrayList<Identity>();
LocalFolderImpl item = new LocalFolderImpl(new File(FilePersister.getFullPathToCourseNodeDirectory(Long.toString(resourceableId), ident)));
if (VFSManager.exists(item)) {
Manager secMgr = ManagerFactory.getManager();
for (VFSItem identityFolder : item.getItems()) {
Identity identity = secMgr.findIdentityByName(identityFolder.getName());
if (identity != null) identities.add(identity);
}
}
return identities;
}
/**
* Removes course node directory including qti.ser files of different users.
* @param resourceableId
* @param ident
* @return
*/
public VFSStatus removeQtiSerFiles(Long resourceableId, String ident) {
if (resourceableId == null || ident == null || ident.length() == 0) return VFSConstants.NO;
LocalFolderImpl item = new LocalFolderImpl(new File(FilePersister.getFullPathToCourseNodeDirectory(Long.toString(resourceableId), ident)));
if(item.canDelete().equals(VFSConstants.YES)) return item.delete();
return VFSConstants.NO;
}
}