/** * <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> * BPS Bildungsportal Sachsen GmbH, http://www.bps-system.de * <p> */ package de.bps.onyx.util; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.IOUtils; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.UserConstants; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.WebappHelper; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.groupsandrights.CourseGroupManager; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.IQSURVCourseNode; import org.olat.course.nodes.iq.IQEditController; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti.QTIResultSet; import org.olat.repository.RepositoryEntry; import de.bps.onyx.plugin.OnyxModule; import de.bps.onyx.plugin.OnyxResultManager; import de.bps.onyx.plugin.wsclient.OnyxExamMode; import de.bps.onyx.plugin.wsclient.OnyxExamModeService; import de.bps.onyx.plugin.wsserver.MapWrapper; import de.bps.onyx.plugin.wsserver.StudentIdsWrapper; import de.bps.onyx.plugin.wsserver.TestState; import de.bps.onyx.plugin.wsserver.TraineeStatusService; import de.bps.webservices.clients.onyxreporter.OnyxReporterConnectorFileNameFilter; public class ExamPool implements Serializable { private static final long serialVersionUID = 4771377056688023915L; private static final String PARAM_FIRSTNAME = "firstname"; private static final String PARAM_LASTNAME = "lastname"; private static final String PARAM_LANGUAGE = "language"; private static final String PARAM_STATUS = "status"; private static final String IS_SURVEY = "isSurvey"; private static final String IS_SYNCHRONIZED = "isSynchronized"; private static final String SHOW_SOLUTION = "showSolution"; public static final String CONTINUATION_ALLOWED = "continuationAllowed"; public static final String SUSPENSION_ALLOWED = "suspendAllowed"; public static final String TEMPLATE_ID = "templateid"; private transient final static OLog log = Tracing.createLoggerFor(ExamPool.class); //mapping for key : assessmentId value: resultSet private final Map<Long, Identity> assessmentIdentityMapping = new ConcurrentHashMap<Long, Identity>(); //mapping for key: identityId value:assessmentId private final Map<Long, Long> identityAssessmentMapping = new ConcurrentHashMap<Long, Long>(); //mapping for key: identity value: state private final Map<Identity, TestState> studentStates = new ConcurrentHashMap<Identity, TestState>(); private final Long testSessionId; // private final Boolean sessionInitializied; private transient final OnyxExamModeService service; private transient ICourse referencedCourse; private transient CourseNode referencedCourseNode; //private String nodeIdent; private Long courseId; private final File directory = new File(WebappHelper.getUserDataRoot()); ExamPool(Long testSessionId) { super(); //this.course = course; this.testSessionId = testSessionId; OnyxExamMode examMode = new OnyxExamMode(); service = examMode.getOnyxExamModeServicesPort(); } /** * This method registers a new exam at the onyx-exam-service, it should be * only called by the {@link ExamPoolManager} and not the proxies * * @param course * @param courseNode */ void initExamPool(ICourse course, CourseNode courseNode) { RepositoryEntry entry = courseNode.getReferencedRepositoryEntry(); this.referencedCourse = course; this.referencedCourseNode = courseNode; //nodeIdent = courseNode.getIdent(); courseId = course.getResourceableId(); if (entry != null) { byte[] contentPackage = getContentPackage(entry); HashMap<String, String> parameterMap = new HashMap<String, String>(); // set allowShowSolution either to the configured value (!= null) or to defaultvalue false if test or survey, if selftest then the default is true Boolean allowShowSolution = courseNode.getModuleConfiguration().getBooleanEntry(IQEditController.CONFIG_KEY_ALLOW_SHOW_SOLUTION); allowShowSolution = allowShowSolution != null ? allowShowSolution : false; parameterMap.put(SHOW_SOLUTION, String.valueOf(allowShowSolution)); // Boolean showFeedback = (Boolean) courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_ALLOW_SHOW_FEEDBACK); // showFeedback = showFeedback != null ? showFeedback : false; // parameterMap.put(SHOW_FEEDBACK, String.valueOf(showFeedback)); parameterMap.put(IS_SURVEY, String.valueOf(referencedCourseNode instanceof IQSURVCourseNode)); Boolean examControl = courseNode.getModuleConfiguration().getBooleanEntry(ExamPoolManager.CONFIG_KEY_EXAM_CONTROL); examControl = examControl != null ? examControl : Boolean.FALSE; //couple the synchronizedStart again to the examMode Boolean synchronizedStart = (Boolean) courseNode.getModuleConfiguration().get(ExamPoolManager.CONFIG_KEY_EXAM_CONTROL_SYNCHRONIZED_START); synchronizedStart = synchronizedStart != null ? synchronizedStart && examControl : false; parameterMap.put(IS_SYNCHRONIZED, String.valueOf(synchronizedStart)); parameterMap.put(CONTINUATION_ALLOWED, Boolean.toString(!examControl.booleanValue())); Boolean allowSuspension = courseNode.getModuleConfiguration().getBooleanEntry(IQEditController.CONFIG_KEY_ALLOW_SUSPENSION_ALLOWED); allowSuspension = allowSuspension != null ? allowSuspension : false; parameterMap.put(SUSPENSION_ALLOWED, String.valueOf(allowSuspension)); String templateId = courseNode.getModuleConfiguration().getStringValue(IQEditController.CONFIG_KEY_TEMPLATE); if (templateId != null) { parameterMap.put(TEMPLATE_ID, templateId); } MapWrapper wrapper = new MapWrapper(); wrapper.setMap(parameterMap); String providerId = CoreSpringFactory.getImpl(OnyxModule.class).getConfigName(); Long result = service.registerTest(testSessionId, providerId, contentPackage, wrapper); log.info("Init result : " + TestState.getState(result) + " for " + testSessionId + " , provider " + providerId + " and parameters " + parameterMap); } else { log.warn("unable to register new test, no test in course-node"); } refreshGroups(); } private void refreshGroups() { if (referencedCourse == null) { referencedCourse = CourseFactory.loadCourse(courseId); } CourseGroupManager man = referencedCourse.getCourseEnvironment().getCourseGroupManager(); List<Identity> participants = man.getParticipantsFromBusinessGroups(); for (Identity participant : participants) { if (!studentStates.containsKey(participant)) { addStudent(participant, null); } } } void addStudent(Identity student, TestState state) { studentStates.put(student, state != null ? state : TestState.NOT_ENTERED); } Long registerStudentTest(Identity student, QTIResultSet resultSet, TestState state) { Long result = null; addStudent(student, (state != null ? state : TestState.WAITING)); //map this assessment to it's id assessmentIdentityMapping.put(resultSet.getAssessmentID(), student); //map this student to his / her assessment identityAssessmentMapping.put(student.getKey(), resultSet.getAssessmentID()); HashMap<String, String> parameterMap = new HashMap<String, String>(); parameterMap.put(PARAM_LANGUAGE, student.getUser().getPreferences().getLanguage()); parameterMap.put(PARAM_FIRSTNAME, student.getUser().getProperty(UserConstants.FIRSTNAME, null)); parameterMap.put(PARAM_LASTNAME, student.getUser().getProperty(UserConstants.LASTNAME, null)); byte[] recommitedFiles = new byte[0]; if (resultSet.getSuspended()) { log.info("Try to recreate for student " + student.getName() + " and suspended assessment : " + resultSet.getAssessmentID()); //addStudent(student, TestState.SUSPENDED); parameterMap.put(PARAM_STATUS, String.valueOf(TestState.RESUME_SUSPENDED.getValue())); String assessmentType = referencedCourseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString(); String path = null; Boolean isSurvey = false; File xml = null; if (referencedCourseNode instanceof IQSURVCourseNode) { isSurvey = true; } if (isSurvey) { OlatRootFolderImpl courseRootContainer = referencedCourse.getCourseEnvironment().getCourseBaseContainer(); path = courseRootContainer.getBasefile() + File.separator + referencedCourseNode.getIdent() + File.separator; xml = new File(path); } else { path = OnyxResultManager.getResReporting() + File.separator + student.getName() + File.separator + assessmentType + File.separator; xml = new File(directory, path); } if (xml != null && xml.exists()) { File commitMe = null; File[] allXmls = xml.listFiles(new OnyxReporterConnectorFileNameFilter(referencedCourseNode.getIdent(), String.valueOf(resultSet .getAssessmentID()))); if (allXmls != null && allXmls.length > 0) { for (File file : allXmls) { if (file.isFile()) { if(commitMe == null || file.getName().toLowerCase().endsWith(".zip")){ commitMe = file; } } } } if (commitMe != null) { Long fileLength = commitMe.length(); recommitedFiles = new byte[fileLength.intValue()]; java.io.FileInputStream inp = null; try { inp = new java.io.FileInputStream(commitMe); inp.read(recommitedFiles); log.info("Found file for suspended assessment for student " + student.getName() + " and suspended assessment : " + resultSet.getAssessmentID() + " # " + commitMe.getAbsolutePath() + "; lenght " + recommitedFiles.length); } catch (FileNotFoundException e) { log.error("Missing file: " + commitMe.getAbsolutePath(), e); } catch (IOException e) { log.error("Error copying file: " + commitMe.getAbsolutePath(), e); } finally { IOUtils.closeQuietly(inp); } } else { log.info("Did not find files for suspended assessment for student " + student.getName() + " and suspended assessment : " + resultSet.getAssessmentID() + " at " + xml.getAbsolutePath() + " an filter-options " + referencedCourseNode.getIdent() + " and " + resultSet.getAssessmentID()); } } else { log.info("Did not find resreporting folder for student " + student.getName() + " and suspended assessment : " + resultSet.getAssessmentID() + " at " + xml.getAbsolutePath()); } } else { log.info("Assessment not suspended, nothing to restore."); } MapWrapper parameterWrapper = new MapWrapper(); parameterWrapper.setMap(parameterMap); result = service.registerStudent(testSessionId, resultSet.getAssessmentID(), recommitedFiles, parameterWrapper); log.info("Tried to register student with assessmentId" + resultSet.getAssessmentID() + " to test: " + testSessionId + " resulting in : " + TestState.getState(result)); return result; } QTIResultSet getAssessmentForStudent(Identity identity) { QTIResultSet result = null; Long assessmentId = identityAssessmentMapping.get(identity.getKey()); if (assessmentId != null) { result = OnyxResultManager.getResultSet(assessmentId); } return result; } /** * Use this method to modify the currently hold states of the students, this * method should be called by the {@link TraineeStatusService} to keep the * shown states in sync with their states in the test * * @param identities * @param state */ void changeExamState(List<Identity> identities, TestState state) { for (Identity identity : identities) { changeExamState(identity, state); } } /** * Use this method to modify the currently hold state of the student, this * method should be called by the {@link TraineeStatusService} to keep the * shown states in sync with their states in the test * * @param identities * @param state */ void changeExamState(Identity identity, TestState state) { if (studentStates.containsKey(identity)) { studentStates.put(identity, state); } } /** * Use this method to send the requested state-changes for the given users * to the test. The effects of this call will then be set by the TestPlayer * (Onyx) with the help of the {@link TraineeStatusService} * * @param identities * @param state */ void controllExam(List<Identity> identities, TestState state) { StudentIdsWrapper idWrapper = new StudentIdsWrapper(); ArrayList<Long> assessmentIds = new ArrayList<Long>(); for (Identity identity : identities) { Long assessmentId = identityAssessmentMapping.get(identity.getKey()); assessmentIds.add(assessmentId); } idWrapper.setStudentsIds(assessmentIds); HashMap<String, String> parameterMap = new HashMap<String, String>(); parameterMap.put("addTime", TestState.RESUME_ALLOWED == state ? String.valueOf(10) : String.valueOf(0)); MapWrapper parameterWrapper = new MapWrapper(); parameterWrapper.setMap(parameterMap); Long result = service.testControl(testSessionId, idWrapper, state.getValue(), parameterWrapper); log.info("Tried to control exam : " + testSessionId + " for " + assessmentIds + " and state " + state + " and parameters " + parameterMap + " resulting in " + TestState.getState(result)); } TestState getStudentState(Identity student) { return studentStates.get(student); } Map<Identity, TestState> getStudentStates() { refreshGroups(); return studentStates; } Identity getStudentForAssessment(Long assessmentId) { Identity student = assessmentId != null ? assessmentIdentityMapping.get(assessmentId) : null; return student; } Long getTestSessionId() { return testSessionId; } private byte[] getContentPackage(RepositoryEntry repositoryEntry) { File cpFile = FileResourceManager.getInstance().getFileResource(repositoryEntry.getOlatResource()); if (cpFile == null || !cpFile.exists()) { cpFile = getCP(repositoryEntry); } Long fileLength = cpFile.length(); byte[] contentPackage = new byte[fileLength.intValue()]; java.io.FileInputStream inp = null; try { inp = new java.io.FileInputStream(cpFile); inp.read(contentPackage); } catch (FileNotFoundException e) { log.error("Missing file: " + cpFile.getAbsolutePath(), e); } catch (IOException e) { log.error("Error copying file: " + cpFile.getAbsolutePath(), e); } finally { IOUtils.closeQuietly(inp); } return contentPackage; } /** * Generates a file object for the given re. * * @param repositoryEntry * @return */ private File getCP(RepositoryEntry repositoryEntry) { //get content-package (= onyx test zip-file) OLATResourceable fileResource = repositoryEntry.getOlatResource(); String unzipedDir = FileResourceManager.getInstance().unzipFileResource(fileResource).getAbsolutePath(); String zipdirName = FileResourceManager.ZIPDIR; String testName = repositoryEntry.getResourcename(); String pathToFile = unzipedDir.substring(0, unzipedDir.indexOf(zipdirName)); File onyxTestZip = new File(pathToFile + testName); // <OLATCE-499> if (!onyxTestZip.exists()) { onyxTestZip = new File(pathToFile + "repo.zip"); } // </OLATCE-499> return onyxTestZip; } } /* history: $Log: ExamPool.java,v $ Revision 1.19 2012-05-30 09:16:14 blaw OLATCE-2007 * allow resume of suspended surveys * hidde suspended test-tries in reporter-overview Revision 1.18 2012-05-16 13:30:34 blaw OLATCE-2007 * improved resume of suspended tests Revision 1.17 2012-05-15 14:11:04 blaw OLATCE-2021 * catch all exceptions, log them and then throw them back to onyx in returnwsservice * backup the result-file if errors occurred * refactored onyx-reporter-requests into a new background-task * more load/update cycles for qtiresultsets while saving them Revision 1.16 2012-05-09 16:03:48 blaw OLATCE-2007 * allow suspend and resume of tests Revision 1.15 2012-05-07 13:12:51 laeb OPEN - issue OLATCE-2009: Unterbrechen: Konfiguration "Unterbrechen erlauben" für Onyx-Tests freischalten https://www.bps-system.de/devel/browse/OLATCE-2009 * new test config "suspensionAllowed" Revision 1.14 2012-04-25 13:56:53 blaw OLATCE-1968 * refresh groups, if new groups or users were added to the course after first registration of the exam Revision 1.13 2012-04-10 15:04:38 laeb OLATCE-1980 OnyxExamModeService: Um Nutzerdaten erweitern: Vor- und Zuname Student * added firstname and lastname properties to Onyx registerStudent WS call parameters Revision 1.12 2012-04-10 13:57:48 blaw OLATCE-1425 * more logging Revision 1.11 2012-04-05 13:49:41 blaw OLATCE-1425 * added history * better indention * refactored referencess for ExamPoolManagers to the abstract class * added yesNoDialog for StartExam-function * added more gui-warnings and / or fallback-values if student- or exam-values are not available */