/* * File : Reporter.java * Created : 09-jul-2001 17:04 * By : fbusquets * * JClic - Authoring and playing system for educational activities * * Copyright (C) 2000 - 2005 Francesc Busquets & Departament * d'Educacio de la Generalitat de Catalunya * * 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 2 of the License, or * (at your option) 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 (see the LICENSE file). */ package edu.xtec.jclic.report; import edu.xtec.jclic.Activity; import edu.xtec.jclic.bags.ActivitySequenceElement; import edu.xtec.jclic.project.JClicProject; import edu.xtec.jclic.report.SessionReg.Info; import edu.xtec.util.CompoundListCellRenderer; import edu.xtec.util.Encryption; import edu.xtec.util.Html; import edu.xtec.util.JDomUtility; import edu.xtec.util.Messages; import edu.xtec.util.Options; import edu.xtec.util.StrUtils; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JPasswordField; import javax.swing.JScrollPane; import javax.swing.JTextField; /** * This class implements the basic operations related with the process of the results * obtained by users playing JClic activities. These operations include * users identification, compilation of data coming from the activities, storage of * this data for a posterior use and presentation of summarized results. * @author Francesc Busquets (fbusquets@xtec.cat) * @version 13.09.10 */ public class Reporter extends Object { String userId; String sessionKey; String sessionContext; String groupCodeFilter; String userCodeFilter; String description; Date started; List<SessionReg> sessions; SessionReg currentSession; /** * <I>true</I> if the system was successfully initiated, <I>false</I> otherwise */ public boolean initiated; /** * <I>true</I> when the system is connected to a database with user's data. * When <I>false</I>, a generic user id will be used. */ protected Boolean bUserBased; public static final String ALLOW_CREATE_GROUPS="ALLOW_CREATE_GROUPS", ALLOW_CREATE_USERS="ALLOW_CREATE_USERS", SHOW_GROUP_LIST="SHOW_GROUP_LIST", SHOW_USER_LIST="SHOW_USER_LIST", USER_TABLES="USER_TABLES", TIME_LAP="TIME_LAP"; /** Creates new Reporter */ public Reporter() { sessions=new CopyOnWriteArrayList<SessionReg>(); started=new Date(); initiated=false; } /** * Method use to retrieve specific properties of the reports system * @param key Property key * @param defaultValue Default value for this key * @throws Exception Throwed when the system was unable to read the specified property * @return The value related to this property */ public String getProperty(String key, String defaultValue) throws Exception{ return defaultValue; } /** * Retrieves boolean properties of the reports system * @param key * @param defaultValue * @throws Exception * @return */ public boolean getBooleanProperty(String key, boolean defaultValue) throws Exception{ String s=getProperty(key, new Boolean(defaultValue).toString()); return key==null ? defaultValue : Boolean.valueOf(s).booleanValue(); } public List<GroupData> getGroups() throws Exception{ return null; } public List<UserData> getUsers(String groupId) throws Exception{ return null; } public UserData getUserData(String userId) throws Exception{ return null; } public GroupData getGroupData(String groupId) throws Exception{ return null; } public boolean userBased() throws Exception{ if(bUserBased==null) bUserBased=getBooleanProperty(USER_TABLES, true); return bUserBased.booleanValue(); } protected GroupData promptForNewGroup(Component parent, Messages msg) throws Exception{ JTextField gName=new JTextField(20); JTextField gId=new JTextField(20); JComponent[] jc=new JComponent[]{gName, gId}; String[] ps=new String[]{"report_name_prompt", "report_id_prompt"}; String name; String id; GroupData result=null; while(result==null){ boolean dlgResult=msg.showInputDlg(parent, new String[]{"report_new_group_data"}, ps, jc, "report_new_group"); if(!dlgResult) return null; name=StrUtils.nullableString(gName.getText()); id=StrUtils.nullableString(gId.getText()); if(id==null) msg.showAlert(parent, "report_err_bad_id"); else if(getGroupData(id)!=null) msg.showAlert(parent, "report_err_duplicate_id"); else if(name==null) msg.showAlert(parent, "report_err_bad_name"); else{ result=new GroupData(id, name, null, null); result.setId(newGroup(result)); } } return result; } protected UserData promptForNewUser(Component parent, Messages msg, String groupId) throws Exception{ if(groupId==null){ groupId=promptGroupId(parent, msg); if(groupId==null) return null; } JTextField gName=new JTextField(20); JTextField gId=new JTextField(20); JPasswordField pwf1=new JPasswordField(Messages.MAX_PASSWORD_LENGTH); JPasswordField pwf2=new JPasswordField(Messages.MAX_PASSWORD_LENGTH); JComponent[] jc=new JComponent[]{gName, gId, pwf1, pwf2}; String[] ps=new String[]{"report_name_prompt", "report_id_prompt", "report_pw_prompt", "report_pw_prompt_confirm"}; String name; String id; String pw; UserData result=null; while(result==null){ if(!msg.showInputDlg(parent, new String[]{"report_new_user_data"}, ps, jc, "report_new_user" )) return null; name=StrUtils.nullableString(gName.getText()); id=StrUtils.nullableString(gId.getText()); pw=StrUtils.nullableString(String.copyValueOf(pwf1.getPassword())); boolean pwOk=true; if(pw!=null){ pwOk=pw.equals(StrUtils.nullableString(String.copyValueOf(pwf2.getPassword()))); pw=Encryption.Encrypt(pw); } if(!pwOk) msg.showAlert(parent, "report_err_bad_pwd"); else if(id==null) msg.showAlert(parent, "report_err_bad_id"); else if(getUserData(id)!=null) msg.showAlert(parent, "report_err_duplicate_id"); else if(name==null) msg.showAlert(parent, "report_err_bad_name"); else{ result=new UserData(id, name, null, pw, groupId); result.setId(newUser(result)); } } return result; } protected String promptGroupId(Component parent, final Messages msg) throws Exception{ String groupId=null; if(getGroups().isEmpty()){ String s=msg.get("report_generic_group_name"); newGroup(new GroupData(s, s, null, null)); if(getGroups().isEmpty()) return groupId; } final List<GroupData> vg=getGroups(); final JList list=new JList(); list.setCellRenderer(new CompoundListCellRenderer()); list.setListData(vg.toArray()); list.setSelectedValue(vg.get(0), false); JScrollPane listScroll=new JScrollPane(list); JComponent[] jc; if(getBooleanProperty(ALLOW_CREATE_GROUPS, false)){ JButton btn=new JButton(msg.get("report_new_group")); btn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent evt){ Component cmp=null; if(evt.getSource()!=null && evt.getSource() instanceof Component) cmp=(Component)evt.getSource(); try{ GroupData gd=promptForNewGroup(cmp, msg); if(gd!=null){ vg.add(gd); list.setListData(vg.toArray()); list.setSelectedValue(gd, true); } } catch(Exception ex){ msg.showErrorWarning(cmp, "report_err_creating_group", ex); } } }); jc=new JComponent[]{listScroll, btn}; } else{ jc=new JComponent[]{listScroll}; } if(msg.showInputDlg( parent, new String[] {"report_grouplist_title"}, null, jc, "report_ident_user")){ GroupData gd=(GroupData)list.getSelectedValue(); if(gd!=null){ groupId=gd.getId(); } } return groupId; } public String promptUserId(Component parent, final Messages msg) throws Exception{ if(!userBased()) throw new Exception("No users defined in the database!"); boolean cancel=false; int tries=0; while(userId==null && !cancel && tries++<3){ if(getBooleanProperty(SHOW_USER_LIST, true)){ String gi=null; if(getBooleanProperty(SHOW_GROUP_LIST, true)){ gi=promptGroupId(parent, msg); if(gi==null) return null; } final String groupId=gi; final List<UserData> v=getUsers(groupId); boolean allow_create_users=getBooleanProperty(ALLOW_CREATE_USERS, false); if(v.isEmpty() && !allow_create_users){ msg.showErrorWarning(parent, groupId==null ? "report_err_no_users" : "report_err_no_users_in_group", null); break; } final JList list=new JList(); list.setCellRenderer(new CompoundListCellRenderer()); list.setListData(v.toArray()); if(!v.isEmpty()) list.setSelectedValue(v.get(0), false); JScrollPane listScroll=new JScrollPane(list); JComponent[] jc; if(allow_create_users){ JButton btn=new JButton(msg.get("report_new_user")); btn.setToolTipText(msg.get("report_new_user_tooltip")); btn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent evt){ Component cmp=null; if(evt.getSource()!=null && evt.getSource() instanceof Component) cmp=(Component)evt.getSource(); try{ UserData ud=promptForNewUser(cmp, msg, groupId); if(ud!=null){ v.add(ud); list.setListData(v.toArray()); list.setSelectedValue(ud, true); } } catch(Exception ex){ msg.showErrorWarning(cmp, "report_err_creating_user", ex); } } }); jc=new JComponent[]{listScroll, btn}; } else{ jc=new JComponent[]{listScroll}; } if(!msg.showInputDlg(parent, new String[] {"report_userlist_title"}, null, jc, "report_ident_user")) break; UserData ud=(UserData)list.getSelectedValue(); if(ud==null) break; if(ud.pwd!=null && ud.pwd.length()>0){ String pwd=Encryption.Decrypt(ud.pwd); String inputPwd=msg.showInputDlg(parent, "report_user_has_pwd", "report_pw_prompt", null, "report_ident_user", true); // 22-mai-06: Modified to avoid null password entries // Old version: /* if(inputPwd==null) break; else if(inputPwd.equals(pwd)) userId=ud.getId(); else msg.showErrorWarning(parent, "report_err_invalid_user", null); */ // New version: if(pwd.equals(inputPwd)) userId=ud.getId(); else msg.showErrorWarning(parent, "report_err_invalid_user", null); } else userId=ud.getId(); } else{ JTextField textField=new JTextField(); JPasswordField pwdField=new JPasswordField(Messages.MAX_PASSWORD_LENGTH); if(!msg.showInputDlg( parent, new String[]{"report_select_user"}, new String[]{"report_id_prompt", "report_pw_prompt"}, new JComponent[]{textField, pwdField}, "report_ident_user")) break; String s=StrUtils.nullableString(textField.getText()); if(s==null) break; UserData ud=getUserData(s); boolean userOk=false; if(ud!=null){ String uPwd=StrUtils.nullableString(ud.pwd); if(uPwd==null || Encryption.Decrypt(uPwd).equals(String.copyValueOf(pwdField.getPassword()))) userOk=true; } if(!userOk) msg.showErrorWarning(parent, "report_err_invalid_user", null); else userId = ud==null ? "" : ud.getId(); } } return userId; } public String toHtmlString(Messages msg){ String prefix="report_"; Html html=new Html(3000); Html tb; tb=new Html(3000); tb.doubleCell(msg.get(prefix+"started"), true, msg.getShortDateTimeStr(started), false); tb.doubleCell(msg.get(prefix+"system"), true, description== null ? msg.get(prefix+"system_standard") : description, false); if(userId!=null) tb.doubleCell(msg.get(prefix+"user"), true, userId, false); int numSessions=0; int numSequences=0; int nActivities=0; int nActSolved=0; int nActScore=0; int nActions=0; int percentSolved=0; long tScore=0; long tTime=0L; Iterator<SessionReg> it=sessions.iterator(); while(it.hasNext()){ Info inf=it.next().getInfo(true); if(inf.numSequences>0){ numSessions++; numSequences+=inf.numSequences; if(inf.nActivities>0){ nActivities+=inf.nActivities; nActSolved+=inf.nActSolved; nActions+=inf.nActions; if(inf.nActScore>0){ tScore+=(inf.tScore*inf.nActScore); nActScore+=inf.nActScore; } tTime+=inf.tTime; } } } if(numSequences>0){ if(numSessions>1) tb.doubleCell(msg.get(prefix+"num_projects"), true, msg.getNumber(numSessions), false); tb.doubleCell(msg.get(prefix+"num_sequences"), true, msg.getNumber(numSequences), false); tb.doubleCell(msg.get(prefix+"num_activities"), true, msg.getNumber(nActivities), false); if(nActivities>0){ tb.doubleCell(msg.get(prefix+"num_activities_solved"), true, msg.getNumber(nActSolved)+" ("+msg.getPercent(nActSolved*100/nActivities)+")", false); if(nActScore>0) tb.doubleCell(msg.get(prefix+"global_score"), true, msg.getPercent(tScore/nActScore), false); tb.doubleCell(msg.get(prefix+"total_time"), true, msg.getHmsTime(tTime), false); tb.doubleCell(msg.get(prefix+"num_actions"), true, msg.getNumber(nActions), false); } html.append(Html.table(tb.toString(), null, 0, 2, -1, null, false)).append(Html.NBSP); StringBuilder tbs=new StringBuilder(); it=sessions.iterator(); while(it.hasNext()){ SessionReg sr=it.next(); if(sr.getInfo(false).numSequences>0) tbs.append(sr.toHtmlString(msg, false, numSessions>1)); } html.append(Html.table(tbs.substring(0), null, 1, 2, -1, null, false)); } else{ html.append(Html.table(tb.toString(), null, 0, 2, -1, null, false)); html.br().bold(msg.get(prefix+"no_activities")); } return html.toString(); } public static final String ELEMENT_NAME="reporter"; public static final String USER_ID="user", KEY="key", CONTEXT="context", GROUP_CODE_FILTER="groupCodeFilter", USER_CODE_FILTER="userCodeFilter"; public void init(HashMap properties, Component parent, Messages msg) throws Exception{ userId=(String)properties.get(USER_ID); sessionKey=(String)properties.get(KEY); sessionContext=(String)properties.get(CONTEXT); groupCodeFilter=(String)properties.get(GROUP_CODE_FILTER); userCodeFilter=(String)properties.get(USER_CODE_FILTER); initiated=true; } public static Reporter getReporter(HashMap properties, Component parent, Messages msg) throws Exception{ String className; if(properties==null) throw new IllegalArgumentException("Null properties passed to \"getReporter\""); if((className=(String)properties.get(JDomUtility.CLASS))==null) throw new IllegalArgumentException("Properties passed to \"getReporter\" with null class name"); Class reporterClass=Class.forName(className); Reporter rep=(Reporter)reporterClass.newInstance(); rep.init(properties, parent, msg); return rep; } public static Reporter getReporter(String className, String strProperties, Component parent, Messages msg) throws Exception{ if(className==null || className.length()==0) throw new IllegalArgumentException("Properties passed to \"getReporter\" with null class name"); if(className.indexOf('.')<0){ className="edu.xtec.jclic.report."+className; } HashMap<String, Object> properties=new HashMap<String, Object>(); properties.put(JDomUtility.CLASS, className); Options.strToMap(strProperties, properties, ";", '=', false); return getReporter(properties, parent, msg); } @Override protected void finalize() throws Throwable{ try { end(); } finally { super.finalize(); } } public void end(){ endSession(); } public void endSequence(){ if(currentSession!=null) currentSession.endSequence(); } public void endSession(){ endSequence(); currentSession=null; } public String newGroup(GroupData gd) throws Exception{ throw new Exception("No database!"); } public String newUser(UserData ud) throws Exception{ throw new Exception("No database!"); } public void newSession(JClicProject jcp, Component parent, Messages msg){ endSession(); currentSession=new SessionReg(jcp); sessions.add(currentSession); } public void newSequence(ActivitySequenceElement ase){ if(currentSession!=null) currentSession.newSequence(ase); } public void newActivity(Activity act){ if(currentSession!=null) currentSession.newActivity(act); } public void endActivity(int score, int numActions, boolean solved){ if(currentSession!=null) currentSession.endActivity(score, numActions, solved); } public void newAction(String type, String source, String dest, boolean ok){ if(currentSession!=null) currentSession.newAction(type, source, dest, ok); } public edu.xtec.jclic.report.SequenceReg.Info getCurrentSequenceInfo(){ return currentSession==null ? null : currentSession.getCurrentSequenceInfo(); } public String getCurrentSequenceTag(){ return currentSession==null ? null : currentSession.getCurrentSequenceTag(); } }