/********************************************************************************** * $URL: $ * $Id: $ *********************************************************************************** * * Author: Charles Hedrick, hedrick@rutgers.edu * * Copyright (c) 2010 Rutgers, the State University of New Jersey * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.lessonbuildertool.service; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.HashSet; import java.util.TreeSet; import java.util.Comparator; import java.util.Date; import java.util.Map; import java.util.Iterator; import java.lang.reflect.Method; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.content.cover.ContentHostingService; import org.sakaiproject.util.Validator; import org.sakaiproject.lessonbuildertool.service.LessonSubmission; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean.UrlItem; import org.sakaiproject.assignment2.model.constants.AssignmentConstants; import org.sakaiproject.assignment2.model.Assignment2; import org.sakaiproject.assignment2.model.AssignmentAttachment; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.InUseException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.tool.api.Session; import org.sakaiproject.db.cover.SqlService; import org.sakaiproject.db.api.SqlReader; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.site.api.ToolConfiguration; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.CacheRefresher; import org.sakaiproject.memory.api.MemoryService; import uk.org.ponder.messageutil.MessageLocator; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.service.gradebook.shared.*; import org.sakaiproject.db.cover.SqlService; import org.sakaiproject.db.api.SqlReader; import java.sql.Connection; import java.sql.ResultSet; /** * Interface to Assignment * * @author Charles Hedrick <hedrick@rutgers.edu> * */ // NOTE: almost no other class should import this. We want to be able // to support both forums and jforum. So typically there will be a // forumEntity, but it's injected, and it can be either forum and jforum. // Hence it has to be declared LessonEntity. That leads to a lot of // declarations like LessonEntity forumEntity. In this case forumEntity // means either a ForumEntity or a JForumEntity. We can't just call the // variables lessonEntity because the same module will probably have an // injected class to handle tests and quizes as well. That will eventually // be converted to be a LessonEntity. public class Assignment2Entity implements LessonEntity, AssignmentInterface { class Assignment { String context; Date dueTime; Long gradebookitem; String title; } class AssignGroup { boolean update; Long id; Long version; String groupid; } private static Log log = LogFactory.getLog(Assignment2Entity.class); private static Cache assignmentCache = null; protected static final int DEFAULT_EXPIRATION = 10 * 60; static boolean haveA2 = false; Object dao = null; private SimplePageBean simplePageBean; public void setSimplePageBean(SimplePageBean simplePageBean) { this.simplePageBean = simplePageBean; } private LessonEntity nextEntity = null; public void setNextEntity(LessonEntity e) { nextEntity = e; } public LessonEntity getNextEntity() { return nextEntity; } public void setPrevEntity(LessonEntity e) { e.setNextEntity(this); } static MemoryService memoryService = null; public void setMemoryService(MemoryService m) { memoryService = m; } static MessageLocator messageLocator = null; public void setMessageLocator(MessageLocator m) { messageLocator = m; } static GradebookService gradebookService = null; public void setGradebookService(GradebookService g) { gradebookService = g; } static OptSql optSql = null; Method saveMethod = null; public void init () { // if (ToolManager.getTool("sakai.assignment2") != null) if (ComponentManager.get("org.sakaiproject.assignment2.service.api.Assignment2Service") != null) haveA2 = true; if (haveA2) { assignmentCache = memoryService .newCache("org.sakaiproject.lessonbuildertool.service.Assignment2Entity.cache"); // Unfortunately the interface class is part of the component, not the shared library, // so we can't get to it. The only way to invoke stuff in the DAO is through introspection. dao = ComponentManager.get("org.sakaiproject.assignment2.dao.AssignmentDao"); // get the methods we need from the DAO try { saveMethod = dao.getClass().getMethod("save", Object.class); } catch (Exception f) { System.out.println("assignment2 lessons interface unable to get save method from A2 dao " + f); }; //try { // Method [] methods = dao.getClass().getMethods(); // for (int i = 0; i < methods.length; i++) { // Method method = methods[i]; // if (method.getName().equals("save")) { // saveMethod = method; // } // } // } catch (Exception e) { // System.out.println("getmethod failed " + e); // } String vendor = SqlService.getVendor(); try { optSql = (OptSql) Assignment2Entity.class.getClassLoader().loadClass("org.sakaiproject.lessonbuildertool.service.OptSql" + vendor).newInstance(); } catch (Exception e) { try { optSql = (OptSql) Assignment2Entity.class.getClassLoader().loadClass("org.sakaiproject.lessonbuildertool.service.OptSqlDefault").newInstance(); } catch (Exception ee) { } } } log.info("init()"); } public void destroy() { if (haveA2) assignmentCache.destroy(); assignmentCache = null; log.info("destroy()"); } // to create bean. the bean is used only to call the pseudo-static // methods such as getEntitiesInSite. So type, id, etc are left uninitialized protected Assignment2Entity() { } /* id is a string containing a long in this case */ protected Assignment2Entity(int type, String id, int level) throws NumberFormatException { this.type = type; this.id = Long.parseLong(id); this.level = level; } public String getToolId() { return "sakai.assignment2"; } // the underlying object, something Sakaiish protected Long id; protected int type; protected int level; // not required fields. If we need to look up // the actual objects, lets us cache them protected Assignment assignment; // ref looks like /assignment2/id public Assignment getAssignment(Long id) { return getAssignment(id, false); } public Assignment getAssignment(Long id, boolean nocache) { Assignment ret = (Assignment)assignmentCache.get(id); if (!nocache && ret != null) return ret; Connection connection = null; try { String siteId = ToolManager.getCurrentPlacement().getContext(); connection = SqlService.borrowConnection(); String sql="select context, due_date, gradebook_item_id, title from A2_ASSIGNMENT_T where assignment_id = ? and draft=0 and removed=0"; Object fields[] = new Object[1]; fields[0] = id; List<Assignment>assignments = SqlService.dbRead(connection, sql, fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { Assignment a = new Assignment(); a.context = result.getString(1); a.dueTime = result.getDate(2); a.gradebookitem = result.getLong(3); a.title = result.getString(4); return a; } catch (Exception ignore) {}; return null; } }); if (assignments.size() == 1) { ret = assignments.get(0); if (!ret.context.equals(siteId)) ret = null; // assignment not in current site; security problem } } catch (Exception e) { System.out.println("Assignment2Entity Eexception " + e); ret = null; } finally { try { if (connection != null) SqlService.returnConnection(connection); } catch (Exception ignore) {}; } if (ret != null) assignmentCache.put(id, ret, DEFAULT_EXPIRATION); return ret; } // type of the underlying object public int getType() { return type; } public int getLevel() { return level; } public int getTypeOfGrade() { if (assignment == null) assignment = getAssignment(id); if (assignment == null) return 1; if (assignment.gradebookitem == 0) return 1; else return 3; } // hack for forums. not used for assessments, so always ok public boolean isUsable() { return true; } public String getReference() { return "/" + ASSIGNMENT2 + "/" + id; } public List<LessonEntity> getEntitiesInSite() { return getEntitiesInSite(null); } // find topics in site, but organized by forum public List<LessonEntity> getEntitiesInSite(final SimplePageBean bean) { if (!haveA2) { if (nextEntity != null) return nextEntity.getEntitiesInSite(); else return new ArrayList<LessonEntity>(); } Connection connection = null; List <LessonEntity> ret = new ArrayList<LessonEntity>(); try { String siteId = ToolManager.getCurrentPlacement().getContext(); connection = SqlService.borrowConnection(); String sql="select context, due_date, gradebook_item_id, title, assignment_id from A2_ASSIGNMENT_T where context = ? and draft=0 and removed=0"; Object fields[] = new Object[1]; fields[0] = siteId; ret = SqlService.dbRead(connection, sql, fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { Assignment a = new Assignment(); a.context = result.getString(1); a.dueTime = result.getDate(2); a.gradebookitem = result.getLong(3); a.title = result.getString(4); Assignment2Entity a2 = new Assignment2Entity(TYPE_ASSIGNMENT2, result.getString(5), 1); a2.assignment = a; a2.simplePageBean = bean; return a2; } catch (Exception ignore) {}; return null; } }); } catch (Exception e) { // leave ret as null list } finally { try { if (connection != null) SqlService.returnConnection(connection); } catch (Exception ignore) {}; } if (nextEntity != null) ret.addAll(nextEntity.getEntitiesInSite(bean)); return ret; } public LessonEntity getEntity(String ref) { return getEntity(ref, null); } public LessonEntity getEntity(String ref, SimplePageBean bean) { int i = ref.indexOf("/",1); String typeString = ref.substring(1, i); String idString = ref.substring(i+1); if (typeString.equals(ASSIGNMENT2)) { Assignment2Entity entity = new Assignment2Entity(TYPE_ASSIGNMENT2, idString, 1); entity.setSimplePageBean(bean); return entity; } else if (nextEntity != null) { return nextEntity.getEntity(ref, simplePageBean); } else return null; } // properties of entities public String getTitle() { if (assignment == null) assignment = getAssignment(id); if (assignment == null) return null; return assignment.title; } // http://heidelberg.rutgers.edu/portal/tool/575de542-a928-41a8-aab0-348a67e2ccc1/student-submit/5 public String getUrl() { if (simplePageBean != null) { return ServerConfigurationService.getToolUrl() + "/" + simplePageBean.getCurrentTool("sakai.assignment2") + "/student-submit/" + id; } Site site = null; try { site = SiteService.getSite(ToolManager.getCurrentPlacement().getContext()); } catch (Exception impossible) { return null; } ToolConfiguration tool = site.getToolForCommonId("sakai.assignment2"); if(tool == null) { return null; } String placement = tool.getId(); return ServerConfigurationService.getToolUrl()+ "/" + placement + "/student-submit/" + id; // following is broken in 2.8.1 // return "/direct/assignment2/" + id; } public Date getDueDate() { if (assignment == null) assignment = getAssignment(id); if (assignment == null) return null; return assignment.dueTime; } // the following methods all take references. So they're in effect static. // They ignore the entity from which they're called. // The reason for not making them a normal method is that many of the // implementations seem to let you set access control and find submissions // from a reference, without needing the actual object. So doing it this // way could save some database activity List<AssignGroup> getAssignGroups() { Connection connection = null; try { connection = SqlService.borrowConnection(); String sql="select assignment_group_id, version, group_ref from A2_ASSIGN_GROUP_T where assignment_id = ?"; Object fields[] = new Object[1]; fields[0] = id; List<AssignGroup>assignGroups = SqlService.dbRead(connection, sql, fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { AssignGroup a = new AssignGroup(); a.id = result.getLong(1); a.version = result.getLong(2); a.groupid = result.getString(3); a.update = false; return a; } catch (Exception ignore) {}; return null; } }); return assignGroups; } catch (Exception e) { return null; } finally { try { if (connection != null) SqlService.returnConnection(connection); } catch (Exception ignore) {}; } } // access control // no longer used, so there's no way to test them public boolean addEntityControl(String siteId, final String groupId) throws IOException { return false; } public boolean removeEntityControl(String siteId, String groupId) throws IOException { return false; } // submission // do we need the data from submission? public boolean needSubmission(){ return true; } public Double toDouble(Object f) { if (f instanceof Double) return (Double)f; else if (f instanceof Float) return ((Float)f).doubleValue(); else return null; } public LessonSubmission getSubmission(String userId) { if (assignment == null) assignment = getAssignment(id); if (assignment == null) { log.warn("can't find assignment " + id); return null; } Connection connection = null; try { connection = SqlService.borrowConnection(); String sql="select completed from A2_SUBMISSION_T where assignment_id = ? and user_id = ?"; Object fields[] = new Object[2]; fields[0] = id; fields[1] = userId; List<String>submissions = SqlService.dbRead(connection, sql, fields, null); if (submissions != null && submissions.size() > 0) { String completed = submissions.get(0); if (!("1".equals(completed) || "true".equals(completed))) return null; if (assignment.gradebookitem == null) return new LessonSubmission(null); // following will give a security error if assignment not released. I think that's better than // checking myself, as that would require fetchign the assignment definition from the gradebook // A2 doesn't seem to save that. Score is scaled, so need * 10 Double score = toDouble(gradebookService.getAssignmentScore(assignment.context, assignment.gradebookitem, userId)); if (score != null) { LessonSubmission ret = new LessonSubmission(score); // shouldn't actually need the string value score = score * 10.0; ret.setGradeString(Long.toString(score.longValue())); return ret; } else return null; } else return null; } catch (Exception e) { return null; } finally { try { if (connection != null) SqlService.returnConnection(connection); } catch (Exception ignore) {}; } } // we can do this for real, but the API will cause us to get all the submissions in full, not just a count. // I think it's cheaper to get the best assessment, since we don't actually care whether it's 1 or >= 1. public int getSubmissionCount(String user) { if (getSubmission(user) == null) return 0; else return 1; } // URL to create a new item. Normally called from the generic entity, not a specific one // can't be null public List<UrlItem> createNewUrls(SimplePageBean bean) { ArrayList<UrlItem> list = new ArrayList<UrlItem>(); if (haveA2) { String tool = bean.getCurrentTool("sakai.assignment2"); if (tool != null) { tool = ServerConfigurationService.getToolUrl()+ "/" + tool + "/assignment"; list.add(new UrlItem(tool, messageLocator.getMessage("simplepage.create_assignment2"))); } } if (nextEntity != null) list.addAll(nextEntity.createNewUrls(bean)); return list; } // URL to edit an existing entity. // Can be null if we can't get one or it isn't needed public String editItemUrl(SimplePageBean bean) { String tool = bean.getCurrentTool("sakai.assignment2"); if (tool == null) return null; return ServerConfigurationService.getToolUrl()+ "/" + tool + "/assignment/" + id; } // for most entities editItem is enough, however tests allow separate editing of // contents and settings. This will be null except in that situation public String editItemSettingsUrl(SimplePageBean bean) { return null; } public boolean objectExists() { if (assignment == null && haveA2) assignment = getAssignment(id); return assignment != null; } public boolean notPublished(String ref) { return false; } // return the list of groups if the item is only accessible to specific groups // null if it's accessible to the whole site. public Collection<String> getGroups(boolean nocache) { List<String>ret = new ArrayList<String>(); List<AssignGroup> assignGroups = getAssignGroups(); for (AssignGroup a: assignGroups) { ret.add(a.groupid); } return ret; } // set the item to be accessible only to the specific groups. // null to make it accessible to the whole site // // We can't access the A2 support code, because it's inside the component,with no // external API. This code was written before I figured out how to use the DAO. // I may be able to rewrite it that way. Note that Oracle needs slightly diffferent // code, so we use a loadable class for some of the SQL statements // public void setGroups(Collection<String> tgroups) { ArrayList<String> groups = null; if (tgroups != null) groups = new ArrayList<String>(tgroups); List<AssignGroup> assignGroups = getAssignGroups(); Iterator<AssignGroup>i = assignGroups.iterator(); while (i.hasNext()) { AssignGroup a = i.next(); if (groups != null && groups.contains(a.groupid)) { groups.remove(a.groupid); a.update = true; // it's in new set, so update it } } final Collection<String> fgroups = groups; final List<AssignGroup> fassignGroups = assignGroups; // fgroups is now stuff to add // fassignGroups is stuff to update or remove // in a transaction SqlService.transact(new Runnable() { public void run() { String updatesql = "update A2_ASSIGN_GROUP_T set version=? where assignment_group_id = ?"; // "insert into A2_ASSIGN_GROUP_T (version, assignment_id, group_ref) values (0, ?, ?)"; String addsql = optSql.Assignment2InsertGroupSql(); String deletesql = "delete from A2_ASSIGN_GROUP_T where assignment_group_id = ?"; for (AssignGroup a: fassignGroups) { if (a.update) { Object fields[] = new Object[2]; fields[0] = a.version + 1; fields[1] = a.id; SqlService.dbWrite(updatesql, fields); } else { Object fields[] = new Object[1]; fields[0] = a.id; SqlService.dbWrite(deletesql, fields); } } if (fgroups != null) for (String g:fgroups) { Object fields[] = new Object[2]; fields[0] = id; fields[1] = g; SqlService.dbWrite(addsql, fields); } } }, "assignment2setgroups"); } // currently assignment 2 does not participate in the fixup. However I'm going to include // the ID anyway, just in case it happens in the future public String getObjectId(){ String title = getTitle(); if (title == null) return null; return "assignment2/" + id + "/" + title; } public String findObject(String objectid, Map<String,String>objectMap, String siteid) { if (!objectid.startsWith("assignment2/")) { if (nextEntity != null) return nextEntity.findObject(objectid, objectMap, siteid); return null; } // isolate forum_topic/NNN from title int i = objectid.indexOf("/", "assignment2/".length()); if (i <= 0) return null; String realobjectid = objectid.substring(0, i); // now see if it's in the map. not currently possible, but who knows String newAssignment = objectMap.get(realobjectid); if (newAssignment != null) return "/" + newAssignment; // sakaiid is /assignment2/ID // Can't find the topic in the map // i is start of title String title = objectid.substring(i+1); String sql="select assignment_id from A2_ASSIGNMENT_T where context = ? and title = ?"; Object fields[] = new Object[2]; fields[0] = siteid; fields[1] = title; List<Long>assignments = SqlService.dbRead(sql, fields, new SqlReader() { public Object readSqlResultRecord(ResultSet result) { try { return (Long)result.getLong(1); } catch (Exception ignore) {}; return null; } }); if (assignments == null || assignments.size() < 1) return null; return "/assignment2/" + assignments.get(0); } public String importObject(String title, String href, String mime){ String contextId = ToolManager.getCurrentPlacement().getContext(); Assignment2 assignment = new Assignment2(); assignment.setContextId(contextId); assignment.setCreateDate(new Date()); assignment.setCreator("ADMIN"); assignment.setDraft(false); assignment.setInstructions(messageLocator.getMessage("simplepage.assign_seeattach")); assignment.setSendSubmissionNotifications(false); assignment.setOpenDate(new Date()); assignment.setRemoved(false); assignment.setSubmissionType(AssignmentConstants.SUBMIT_INLINE_AND_ATTACH); assignment.setGraded(false); assignment.setHonorPledge(false); assignment.setHasAnnouncement(false); assignment.setAddedToSchedule(false); // assignment.setSortIndex(sortIndex); assignment.setTitle(title); assignment.setRequiresSubmission(true); assignment.setNumSubmissionsAllowed(1); // Set <Assignment2> assignSet = new HashSet<Assignment2>(); // assignSet.add(assignment); String newAttRef = null; // in theory we should probably copy this into the attachment area. But // if there are any relative URLs in it, they're fail in the copy. So // unless there are problems, we leave it where it is. This code to make // the copy has been tested and should work. if (false) { try { ContentResource oldAttachment = ContentHostingService.getResource(href); String toolTitle = "Assignments 2"; String name = Validator.escapeResourceName(oldAttachment.getProperties().getProperty(ResourceProperties.PROP_DISPLAY_NAME)); String type = oldAttachment.getContentType(); byte[] content = oldAttachment.getContent(); ResourceProperties properties = oldAttachment.getProperties(); ContentResource newResource = ContentHostingService.addAttachmentResource(name, contextId, toolTitle, type, content, properties); newAttRef = newResource.getId(); } catch (Exception e) { System.out.println("unable to make attachment resource " + e); } } newAttRef = href; AssignmentAttachment attachment = new AssignmentAttachment(assignment, newAttRef); // Set<AssignmentAttachment> attachments = new HashSet<AssignmentAttachment>(); // attachments.add(new AssignmentAttachment(assignment, "/content/" + href)); try { saveMethod.invoke(dao, assignment); saveMethod.invoke(dao, attachment); return "/assignment2/" + assignment.getId(); } catch (Exception e) { System.out.println("invoke failed " + e); } return null; } public String getSiteId() { // can't use getassignment because it assumes we are working // with current site String sql="select context from A2_ASSIGNMENT_T where assignment_id = ?"; Object fields[] = new Object[1]; fields[0] = id; List<String> contexts = SqlService.dbRead(sql, fields, null); if (contexts != null && contexts.size() > 0) return contexts.get(0); return null; } }