/**
* <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.webservices.clients.onyxreporter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import org.apache.commons.io.IOUtils;
import org.olat.core.CoreSpringFactory;
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.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
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.security.SSLConfigurationModule;
//<ONYX-705>
public class OnyxReporterConnector {
//<OLATCE-1124>
private static final String FOUR = "4";
private static final String ONE = "1";
private static final String FIVE = "5";
//</OLATCE-1124>
//<OLATCE-1089>
private static final String NONAME = "NONAME";
//</OLATCE-1089>
private String surveyFolderPath;
private final static OLog log = Tracing.createLoggerFor(OnyxReporterConnector.class);
private final OnyxReporterClient connector;
private final Pattern pattern = Pattern.compile("(.*v)(.*)\\.(xml|zip)");
public OnyxReporterConnector() throws OnyxReporterException {
//TODO check if available
if(isServiceAvailable(OnyxReporterTarget.getTarget())) {
connector = new OnyxReporterClient();
} else {
//<OLATCE-1124>
log.error("OnyxReporterService is unavailable! Tried to use: "+OnyxReporterTarget.getTarget());
//</OLATCE-1124>
throw new OnyxReporterException("Unable to connect to OnyxReporter");
}
}
/**
* Delivers a map with all possible outcome-variables of this onyx test.
* @param node The course node with the Onyx test.
* @return A map with all outcome-variables with name as key and type as value.
*/
//<OLATCE-1012> split the method
// <OLATBPS-363>
public Map<String, String> getPossibleOutcomeVariables(CourseNode node) throws OnyxReporterException {
// </OLATBPS-363>
RepositoryEntry repositoryEntry = node.getReferencedRepositoryEntry();
return this.getPossibleOutcomeVariables(repositoryEntry);
}
//<OLATCE-1012>
//<OLATCE-1012>
/**
* Delivers a map with all possible outcome-variables of this onyx test.
* @param Repoentry with the Onyx test.
* @return A map with all outcome-variables with name as key and type as value.
*/
public Map<String, String> getPossibleOutcomeVariables(RepositoryEntry entry) throws OnyxReporterException {
OnyxReporterServices reporterService = connector.getService();
HashMap<String, String> results;
try {
byte[] contentPackage = getContentPackage(entry);
results = reporterService.getResultVariables(1, contentPackage, new HashMapWrapper()).getMap();
} catch (OnyxReporterException e) {
log.error("Error in getPossibleOutcomeVariables reporter conversation! RepositoryEntry: " + entry.getResourceableId(), e);
results = new HashMap<String, String>();
} catch (Exception e) {
log.error("Unexpected error in getPossibleOutcomeVariables reporter conversation! RepositoryEntry: " + entry.getResourceableId(), e);
results = new HashMap<String, String>();
}
return results;
}
//<OLATCE-1012>
// <OLATBPS-363>
public Map<String, String> getResults(File resultXml, CourseNode node) throws OnyxReporterException {
// </OLATBPS-363>
return getResults(resultXml, node, null);
}
public Map<String, String> getResults(AssessableCourseNode node, Identity identity) throws OnyxReporterException {
//<OLATCE-1073>
File resultXml = getResultFile(identity.getName(), node.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString(), node, 0);
return getResults(resultXml, node, identity);
//</OLATCE-1073>
}
// <OLATBPS-363>
public Map<String, String> getResults(File resultXml, CourseNode node, Identity identity) throws OnyxReporterException {
// </OLATBPS-363>
//<OLATCE-1073>
if(resultXml == null){
log.info("Missing resultFile! For "+(identity!=null?identity.getName():"NULL")+" node : "+(node!=null?(node.getShortName()+":"+node.getIdent()):"NULL"));
return new HashMap<String, String>(0);
}
//</OLATCE-1073>
RepositoryEntry repositoryEntry = node.getReferencedRepositoryEntry();
OnyxReporterServices reporterService = connector.getService();
/** ARM SITE **/
String[] dlh = armSite(reporterService, identity, ReporterRole.AUTHOR);
String secret = dlh[0];
String sessionId = dlh[1];
/** Prepare data **/
ArrayList<ResultsForStudent> resForStudents = new ArrayList<ResultsForStudent>(4);
resForStudents.add(getStudentWithResult(identity, resultXml));
ResultsForStudentsWrapper wrapper = new ResultsForStudentsWrapper();
wrapper.setStudents(resForStudents);
/** INITIATE SITE **/
reporterService.initiateSite(1, sessionId, secret, wrapper, getContentPackage(repositoryEntry), new HashMapWrapper());
HashMapWrapper mapWrapper = reporterService.getResultValues(1, sessionId, secret, new HashMapWrapper(), new HashMapWrapper());
Map<String, String> results;
try {
results = mapWrapper.getMap();
} catch (OnyxReporterException e) {
log.error("Error in getResults reporter conversation! Session: " + sessionId + ", Identity: " + identity.getName(), e);
throw new OnyxReporterException("Error getting results for test! Session: " + sessionId, e);
}
return results;
}
/**
* This method starts the OnyxReporter and returns the link to it.
*
* @param students
* The students to show the results for.
* @param node
* The AssessableCourseNode to get the nodeId and to get the
* (OnyxTest) RepositoryEntry.
* @param role
* defines for which role and which resulting view the reporter
* should be called
* @param ureq
* The UserRequest for getting the identity and role of the
* current user.
* @return the Link to the reporter.
*/
public String startReporterGUI(Identity caller, List<Identity> students, CourseNode node, Long assessmentId, ReporterRole role)
throws OnyxReporterException {
String link = "";
RepositoryEntry repositoryEntry = node.getReferencedRepositoryEntry();
ArrayList<ResultsForStudent> resForStudents = null;
if(surveyFolderPath == null){
resForStudents = getStudentsWithResults(students, node, assessmentId);
} else {
resForStudents = getAnonymizedStudentsWithResultsForSurvey(node.getIdent());
}
try {
OnyxReporterServices reporterService = connector.getService();
HashMapWrapper mapWrapper = new HashMapWrapper();
if (ReporterRole.ASSESSMENT == role) {
HashMap<String, String> underlyingMap = new HashMap<String, String>();
if (assessmentId != null) {
underlyingMap.put("assessmentID", String.valueOf(assessmentId));
}
String providerID = CoreSpringFactory.getImpl(OnyxModule.class).getConfigName();
underlyingMap.put("providerID", providerID);
mapWrapper.setMap(underlyingMap);
}
String[] dlh = armSite(reporterService, caller, role, mapWrapper);
byte[] contentPackage = getContentPackage(repositoryEntry);
ResultsForStudentsWrapper wrapper = new ResultsForStudentsWrapper();
wrapper.setStudents(resForStudents);
link = reporterService.initiateSite(1, dlh[1], dlh[0], wrapper, contentPackage, mapWrapper);
if (link == null) {
throw new OnyxReporterException("Unable to start ReporterGUI! Could not resolve reporter URL!");
} else if (link.indexOf("reportererror") >= 0) {
// use error link to show reporter error page
} else {
//<OLATCE-1124>
if (ReporterRole.REPORTING == role) {
link += FIVE; // view 5 for reporting view / statistical evaluation
} else if (ReporterRole.STUDENT == role) {
link += ONE; // view 1 (single learner view)
} else {
link += FOUR; // view 4 (all learners overview)
}
//</OLATCE-1124>
//add params
link += "?sid=" + dlh[1] + "&secret=" + dlh[0];
//switch to the student view of a specified student
if (ReporterRole.STUDENT == role) {
//link += "&uid="+ students.get(0).getKey();
link += "&uid=" + assessmentId;
}
// add language information
final String lang = caller.getUser().getPreferences().getLanguage();
if (lang != null && !lang.isEmpty()) {
link += "&lang=" + lang;
}
}
} catch (Exception e) {
throw new OnyxReporterException("Unable to start ReporterGUI!", e);
}
return link;
}
// <OLATCE-498>
public boolean hasResults(String username, String assessmentType, CourseNode node) {
return getResultFile(username, assessmentType, node, 0) != null;
}
// </OLATCE-498>
public String startReporterGUIForSurvey(Identity caller, CourseNode node, String resultsPath) throws OnyxReporterException{
this.surveyFolderPath = resultsPath;
//<OLATCE-1124>
return startReporterGUI(caller, null, node, null, ReporterRole.REPORTING);
//</OLATCE-1124>
}
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;
}
private String[] armSite(OnyxReporterServices reporterService, Identity caller, ReporterRole role) {
return armSite(reporterService, caller, role, new HashMapWrapper());
}
private String[] armSite(OnyxReporterServices reporterService, Identity caller, ReporterRole role, HashMapWrapper wrapper) {
String secret = "" + new Random().nextLong();
//<OLATCE-1089>
String lastname = caller.getUser().getProperty(UserConstants.LASTNAME, null);
String firstname = caller.getUser().getProperty(UserConstants.FIRSTNAME,null);
lastname=lastname!=null&&lastname.length()>0?lastname:NONAME;
firstname=firstname!=null&&firstname.length()>0?firstname:NONAME;
String reporterSessionId = reporterService.armSite(1, caller.getName(), role.getKey(), secret,
lastname, firstname, wrapper);
//</OLATCE-1089>
return new String[]{secret, reporterSessionId!=null?reporterSessionId:"dummy"};
}
private ResultsForStudent getStudentWithResult(Identity student, File resultFile){
ResultsForStudent resForStudent = null;
Long fileLength = resultFile.length();
byte[] resultFileStream = new byte[fileLength.intValue()];
java.io.FileInputStream inp = null;
try {
inp = new java.io.FileInputStream(resultFile);
inp.read(resultFileStream);
//<OLATCE-1089>
String lastname = student.getUser().getProperty(UserConstants.LASTNAME, null);
String firstname = student.getUser().getProperty(UserConstants.FIRSTNAME,null);
lastname=lastname!=null&&lastname.length()>0?lastname:NONAME;
firstname=firstname!=null&&firstname.length()>0?firstname:NONAME;
resForStudent = new ResultsForStudent();
//<OLATCE-1169>
resForStudent.setFirstname(firstname);
resForStudent.setLastname(lastname);
//</OLATCE-1169>
//</OLATCE-1089>
//resForStudent.setStudentId(String.valueOf(student.getKey()));
String filename = resultFile.getName();
Matcher matcher = pattern.matcher(filename);
String assessmentId = null;
if (matcher.matches()) {
assessmentId = matcher.group(2);
} else {
final Long key = student.getKey();
log.warn("Could not determine assessment ID from unexpected file name " + filename);
assessmentId = String.valueOf(key);
}
resForStudent.setStudentId(assessmentId);
resForStudent.setGroupname("");
resForStudent.setTutorname("");
resForStudent.setResultsFile(resultFileStream);
} catch (FileNotFoundException e) {
log.error("Missing file: "+resultFile.getAbsolutePath(),e);
} catch (IOException e) {
log.error("Error copying file: "+resultFile.getAbsolutePath(),e);
} finally {
IOUtils.closeQuietly(inp);
}
return resForStudent;
}
private ArrayList<ResultsForStudent> getStudentsWithResults(List<Identity> students, CourseNode node, Long assessmentId){
ArrayList<ResultsForStudent> resForStudents = new ArrayList<ResultsForStudent>();
for(Identity student : students){
File resultFile = getResultFile(student.getName(), node.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString(), node,
assessmentId != null ? assessmentId : 0);
//<OLATCE-1048>
if(resultFile != null)
{
resForStudents.add(getStudentWithResult(student, resultFile));
}
//</OLATCE-1048>
}
return resForStudents;
}
/**
* For every result xml file found in the survey folder a dummy student is created.
* @param nodeId
* @return
*/
private ArrayList<ResultsForStudent> getAnonymizedStudentsWithResultsForSurvey(String nodeId) {
ArrayList<ResultsForStudent> serviceStudents = new ArrayList<ResultsForStudent>();
File directory = new File(this.surveyFolderPath);
Long fileLength;
File resultFile;
if(directory.exists()) {
String[] allXmls = directory.list(new OnyxReporterConnectorFileNameFilter(nodeId));
if (allXmls != null && allXmls.length > 0) {
int id = 0;
for (String xmlFileName : allXmls) {
ResultsForStudent serviceStudent = new ResultsForStudent();
serviceStudent.setFirstname("");
serviceStudent.setLastname("");
serviceStudent.setGroupname("");
serviceStudent.setTutorname("");
serviceStudent.setStudentId("st" + id);
resultFile = new File(this.surveyFolderPath + xmlFileName);
fileLength = resultFile.length();
byte[] resultFileStream = new byte[fileLength.intValue()];
java.io.FileInputStream inp;
try {
inp = new java.io.FileInputStream(resultFile);
inp.read(resultFileStream);
serviceStudent.setResultsFile(resultFileStream);
serviceStudents.add(serviceStudent);
id++;
} catch (FileNotFoundException e) {
log.error("Missing file: "+resultFile.getAbsolutePath(),e);
} catch (IOException e) {
log.error("Error copying file: "+resultFile.getAbsolutePath(),e);
}
}
}
}
return serviceStudents;
}
public static String getFilePath(String username, String assessmentType) {
new File(WebappHelper.getUserDataRoot());
return OnyxResultManager.getResReporting() + File.separator + username + File.separator + assessmentType + File.separator;
}
public static File getResultFileOrNull(QTIResultSet set, CourseNode node) {
if (set == null || node == null) {
return null;
}
return getResultFileOrNull(set.getIdentity().getName(), node.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString(), node,
set.getAssessmentID());
}
private static File getResultFileOrNull(String username, String assessmentType, CourseNode node, long assessmentId) {
File xml = null;
String prefix;
String path = getFilePath(username, assessmentType);
//if an assessment id was given, use the corresponding file
if (assessmentId != 0) {
prefix = OnyxResultManager.getResultsFilenamePrefix(path, node, assessmentId);
xml = new File(WebappHelper.getUserDataRoot(), prefix + OnyxResultManager.SUFFIX_ZIP);
if (xml.exists()) {
return xml;
}
// fall back to "old" xml implementation
xml = new File(WebappHelper.getUserDataRoot(), prefix + OnyxResultManager.SUFFIX_XML);
}
return xml;
}
private File getResultFile(String username, String assessmentType, CourseNode node, long assessmentId) {
File fUserdataRoot = new File(WebappHelper.getUserDataRoot());
String path = getFilePath(username, assessmentType);
File xml = getResultFileOrNull(username, assessmentType, node, assessmentId);
//otherwise search the newest result file with this node id in this directory
if (xml == null || !(xml.exists())) {
File directory = new File(fUserdataRoot, path);
String[] allXmls = directory.list(new OnyxReporterConnectorFileNameFilter(node.getIdent()));
if (allXmls != null && allXmls.length > 0) {
File newestXml = new File(fUserdataRoot, path + allXmls[0]);
File newestZip = null;
/*
* Search for newest file in array. If ZIP files are found,
* prefer them. Use XML files otherwise.
*/
for (String xmlFileName : allXmls) {
File xmlFile = new File(fUserdataRoot, path + xmlFileName);
Matcher matcher = pattern.matcher(xmlFileName);
String currentAssessmentId = null;
if (matcher.matches()) {
currentAssessmentId = matcher.group(2);
QTIResultSet resultSet = OnyxResultManager.getResultSet(Long.parseLong(currentAssessmentId));
if (resultSet != null && !resultSet.getSuspended()) {
if (xmlFileName.endsWith(OnyxResultManager.SUFFIX_ZIP)) {
if (newestZip == null) {
newestZip = xmlFile;
} else {
if (xmlFile.lastModified() > newestZip.lastModified()) {
newestZip = xmlFile;
}
}
} else if (xmlFile.lastModified() > newestXml.lastModified()) {
newestXml = xmlFile;
}
} else {
log.info("Skip suspended result : " + xmlFile);
}
}
}
if (newestZip != null) {
xml = newestZip;
} else {
xml = newestXml;
}
}
}
if (xml == null || !(xml.exists())) {
//<OLATCE-1048>
xml = null;
//</OLATCE-1048>
//log.error("There is no file for this test and student "+username+" assessmentType: "+assessmentType+ " nodeId: "+nodeId+" assessmentId: "+assessmentId);
}
return xml;
}
/**
* 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;
}
private boolean isServiceAvailable(String target) {
HostnameVerifier hv = new HostnameVerifier() {
@Override
public boolean verify(String urlHostName, SSLSession session) {
if (urlHostName.equals(session.getPeerHost())) {
return true;
} else {
return false;
}
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hv);
try {
URL url = new URL(target + "?wsdl");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
if (con instanceof HttpsURLConnection) {
HttpsURLConnection sslconn = (HttpsURLConnection) con;
SSLContext context = SSLContext.getInstance("SSL");
context.init(SSLConfigurationModule.getKeyManagers(),
SSLConfigurationModule.getTrustManagers(),
new java.security.SecureRandom());
sslconn.setSSLSocketFactory(context.getSocketFactory());
sslconn.connect();
if (sslconn.getResponseCode() == HttpURLConnection.HTTP_OK) {
sslconn.disconnect();
return true;
}
} else {
con.connect();
if (con.getResponseCode() == HttpURLConnection.HTTP_OK) {
con.disconnect();
return true;
}
}
} catch (Exception e) {
log.error("Error while trying to connect to webservice: " + target, e);
}
return false;
}
//<OLATCE-1124>
public boolean hasAnyResults(boolean forSurvey,List<Identity> forStudents, String surveyFolder, CourseNode node){
boolean hasResults = false;
FilenameFilter filter = new OnyxReporterConnectorFileNameFilter(node.getIdent());
File directory = new File(WebappHelper.getUserDataRoot());
if(forSurvey){
File surveyDir = new File(surveyFolder);
if(surveyDir.exists()) {
String[] allXmls = surveyDir.list(filter);
if (allXmls != null && allXmls.length > 0) {
hasResults = true;
}
}
} else {
String assessmentType = node.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString();
String path = null;
for(Identity student : forStudents){
path = OnyxResultManager.getResReporting() + File.separator + student.getName() + File.separator
+ assessmentType + File.separator;
File xml = new File(directory, path);
if (xml != null && xml.exists()) {
String[] allXmls = xml.list(filter);
if (allXmls != null && allXmls.length > 0) {
hasResults = true;
break;
}
}
}
}
return hasResults;
}
//</OLATCE-1124>
}
//</ONYX-705>