/** * File ./src/main/java/de/lemo/dms/processing/questions/QUserPathAnalysis.java * Lemo-Data-Management-Server for learning analytics. * Copyright (C) 2013 * Leonard Kappe, Andreas Pursian, Sebastian Schwarzrock, Boris Wenzlaff * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ /** * File ./main/java/de/lemo/dms/processing/questions/QUserPathAnalysis.java * Date 2013-01-24 * Project Lemo Learning Analytics */ package de.lemo.dms.processing.questions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import de.lemo.dms.core.config.ServerConfiguration; import de.lemo.dms.db.IDBHandler; import de.lemo.dms.db.mapping.abstractions.ILogMining; import de.lemo.dms.processing.ELearningObjectType; import de.lemo.dms.processing.MetaParam; import de.lemo.dms.processing.Question; import de.lemo.dms.processing.StudentHelper; import de.lemo.dms.processing.resulttype.ResultListUserPathGraph; import de.lemo.dms.processing.resulttype.UserPathLink; import de.lemo.dms.processing.resulttype.UserPathNode; import de.lemo.dms.processing.resulttype.UserPathObject; /** * Generates a list of Nodes and edges, representing the user-navigation * * @author Sebastian Schwarzrock */ @Path("userpathanalysis") public class QUserPathAnalysis extends Question { /** * Returns a list of Nodes and edges, representing the user-navigation * matching the requirements given by the parameters. * * @see ELearningObjectType * @param courses * List of course-identifiers * @param users * List of user-identifiers * @param types * List of learn object types (see ELearnObjType) * @param considerLogouts * If user-paths should be cut when a logout appears this must be * set to "true". * @param startTime * LongInteger time stamp * @param endTime * LongInteger time stamp * @return */ @POST public ResultListUserPathGraph compute( @FormParam(MetaParam.COURSE_IDS) final List<Long> courses, @FormParam(MetaParam.USER_IDS) List<Long> users, @FormParam(MetaParam.TYPES) final List<String> types, @FormParam(MetaParam.LOGOUT_FLAG) final Boolean considerLogouts, @FormParam(MetaParam.START_TIME) final Long startTime, @FormParam(MetaParam.END_TIME) final Long endTime, @FormParam(MetaParam.GENDER) final List<Long> gender) { validateTimestamps(startTime, endTime); // DB-initialization final IDBHandler dbHandler = ServerConfiguration.getInstance().getMiningDbHandler(); final Session session = dbHandler.getMiningSession(); Criteria criteria; if(users == null || users.size() == 0) { users = new ArrayList<Long>(StudentHelper.getCourseStudentsAliasKeys(courses, gender).values()); } else { Map<Long, Long> userMap = StudentHelper.getCourseStudentsAliasKeys(courses, gender); List<Long> tmp = new ArrayList<Long>(); for(int i = 0; i < users.size(); i++) { tmp.add(userMap.get(users.get(i))); } users = tmp; } // Create criteria for log-file-search criteria = session.createCriteria(ILogMining.class, "log"); criteria.add(Restrictions.between("log.timestamp", startTime, endTime)); criteria.addOrder(Order.asc("log.timestamp")); if (!courses.isEmpty()) { criteria.add(Restrictions.in("log.course.id", courses)); } if (!users.isEmpty()) { criteria.add(Restrictions.in("log.user.id", users)); } @SuppressWarnings("unchecked") final List<ILogMining> list = criteria.list(); Collections.sort(list); this.logger.debug("Total matched entries: " + list.size()); // Map for UserPathObjects final LinkedHashMap<String, UserPathObject> pathObjects = Maps.newLinkedHashMap(); // Map for user histories final HashMap<Long, List<ILogMining>> userHis = Maps.newHashMap(); int skippedUsers = 0; // Generate user histories for (final ILogMining log : list) { if (log.getUser() == null) { skippedUsers++; continue; } boolean typeOk = true; if (!types.isEmpty()) { typeOk = false; for (final String type : types) { /* * XXX why is this checked for every log object and not a * criteria restriction? */ // Check if ILog-object has acceptable learningObjectType if (ELearningObjectType.fromLogMiningType(log).toString().toUpperCase().equals(type)) { typeOk = true; break; } } } if (typeOk) { if (userHis.get(log.getUser().getId()) == null) { // If user is new create a new entry in the hash map and add // log item userHis.put(log.getUser().getId(), new ArrayList<ILogMining>()); userHis.get(log.getUser().getId()).add(log); } else { userHis.get(log.getUser().getId()).add(log); } } } this.logger.debug("Skipped entries with missing user id: " + skippedUsers); int skippedLogs = 0; // Generate paths from user histories for (final List<ILogMining> l : userHis.values()) { String predNode = null; for (int i = 0; i < l.size(); i++) { if ((l.get(i) != null) && (l.get(i).getUser() != null)) { final ILogMining current = l.get(i); final Long learnObjId = current.getLearnObjId(); if (learnObjId == null) { skippedLogs++; continue; } final String learnObjType = ELearningObjectType.fromLogMiningType(current).toString(); final String type = current .getClass() .toString() .substring(current.getClass().toString().lastIndexOf(".") + 1, current.getClass().toString().lastIndexOf("Log")); final String cId = learnObjId + "-" + learnObjType; // Determines whether it's a new path (no predecessor for // current node) or not UserPathObject knownPath; if (predNode != null) { String cIdPos = null; if ((knownPath = pathObjects.get(cId)) == null) { // If the node is new create entry in hash map cIdPos = String.valueOf(pathObjects.size()); pathObjects.put(cId, new UserPathObject(cIdPos, current.getTitle(), 1L, type, Double.valueOf(current.getDuration()), 1L, 0L, 0L, 0L)); } else { // If the node is already known, increase weight pathObjects.get(cId).increaseWeight(Double.valueOf(current.getDuration())); cIdPos = knownPath.getId(); } // Increment or create predecessor edge pathObjects.get(predNode).addEdgeOrIncrement(cIdPos); } else if (pathObjects.get(cId) == null) { final String cIdPos = String.valueOf(pathObjects.size()); pathObjects.put(cId, new UserPathObject(cIdPos, current.getTitle(), 1L, type, Double.valueOf(current.getDuration()), 1L, 0L, 0L, 0L)); } else { pathObjects.get(cId).increaseWeight(Double.valueOf(current.getDuration())); } if (considerLogouts && (current.getDuration() == -1L)) { predNode = null; } else { predNode = cId; } } } } this.logger.debug("Skipped entries with missing learn object id: " + skippedLogs); final ArrayList<UserPathNode> nodes = Lists.newArrayList(); final ArrayList<UserPathLink> links = Lists.newArrayList(); for (final UserPathObject pathEntry : pathObjects.values()) { final UserPathObject path = pathEntry; path.setPathId(pathEntry.getPathId()); nodes.add(new UserPathNode(path)); final String sourcePos = path.getId(); for (final Entry<String, Integer> linkEntry : pathEntry.getEdges().entrySet()) { final UserPathLink link = new UserPathLink(); link.setSource(sourcePos); link.setPathId(path.getPathId()); link.setTarget(linkEntry.getKey()); link.setValue(String.valueOf(linkEntry.getValue())); if (link.getSource() != link.getTarget()) { links.add(link); } } } session.close(); return new ResultListUserPathGraph(nodes, links); } }