/********************************************************************************** * $URL: https://newtools.oirt.rutgers.edu:8443/repos/sakai2.x/sakai/trunk/assignment/assignment-impl/impl/src/java/org/sakaiproject/assignment/impl/AssignmentGradeInfoProvider.java $ * $Id: AssignmentGradeInfoProvider.java 4492 2013-03-22 15:02:00Z willkara $ *********************************************************************************** * * Copyright (c) 2011 The Sakai Foundation * * 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.osedu.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.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Arrays; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroup; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.EntityManager; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.site.api.SiteService; import org.sakaiproject.service.gradebook.shared.ExternalAssignmentProvider; import org.sakaiproject.service.gradebook.shared.GradebookExternalAssessmentService; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.user.api.User; import org.sakaiproject.db.cover.SqlService; import org.sakaiproject.db.api.SqlReader; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.CacheRefresher; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.lessonbuildertool.SimplePageItem; import org.sakaiproject.lessonbuildertool.SimplePage; import org.sakaiproject.lessonbuildertool.model.SimplePageToolDao; public class LessonsGradeInfoProvider implements ExternalAssignmentProvider { private Log log = LogFactory.getLog(LessonsGradeInfoProvider.class); // caching private static Cache cache = null; // currently using 10 sec. The real goal is to prevent continual // reevaluation of items as we follow different paths. I.e. we mostly // care about it during a single transaction. But I'm using the normal // default of 10 min protected static final int DEFAULT_EXPIRATION = 60 * 10; // Sakai Service Beans private GradebookExternalAssessmentService geaService; private SimplePageToolDao dao; private AuthzGroupService authzGroupService; private SecurityService securityService; private MemoryService memoryService; public void init() { cache = memoryService .newCache("org.sakaiproject.lessonbuildertool.service.LessonsGradeInfoProvider.cache"); log.info("INIT and register LessonsGradeInfoProvider"); geaService.registerExternalAssignmentProvider(this); } public void destroy() { log.info("DESTROY and unregister LessonsGradeInfoProvider"); geaService.unregisterExternalAssignmentProvider(getAppKey()); cache.destroy(); cache = null; } public String getAppKey() { return "Lesson Builder"; } // general note: // this code needs to be efficient for large sites // I do my best to use bulk operations // ids look like lesson-builder:comment:NNN // throughout this class you'll see code to find // the item from the id by finding the item number // at the end and that looking up by ID public boolean isAssignmentDefined(String id) { if (!id.startsWith("lesson-builder:")) return false; int i = id.lastIndexOf(":"); if (i < 0) return false; String prefix = id.substring(0, i); String itemNum = id.substring(i+1); if ("lesson-builder".equals(prefix)) { SimplePage page = null; long pageId = 0; try { pageId = Long.parseLong(itemNum); page = dao.getPage(pageId); if (page == null) return false; } catch (Exception e){ return false; } return true; } long itemId = 0; try { itemId = Long.parseLong(itemNum); if (dao.findItem(itemId) == null) return false; } catch (Exception e){ return false; } return true; } // is access to this item restricted by group access public boolean isAssignmentGrouped(String id) { if (!id.startsWith("lesson-builder:")) return false; int i = id.lastIndexOf(":"); if (i < 0) return false; String prefix = id.substring(0, i); String itemNum = id.substring(i+1); if ("lesson-builder".equals(prefix)) { SimplePage page = null; long pageId = 0; try { pageId = Long.parseLong(itemNum); page = dao.getPage(pageId); if (page == null) return false; } catch (Exception e){ return false; } Set<String> groupIds = getPageGroups(pageId, new HashSet<Long>()); if (groupIds == null) return false; return true; } SimplePageItem item = null; long itemId = 0; try { itemId = Long.parseLong(itemNum); item = dao.findItem(itemId); if (item == null) return false; } catch (Exception e){ return false; } Set<String> groupIds = getItemGroups(item); if (groupIds == null) return false; return true; } // my best estimate is about 100 bytes / call. The maximum likely chain is // 100. So that could put 10K on the stack. With 1 G stacks I think that's OK /* main entry to this is getItemGroups(item, []) find all the groups allowed to use this item null means no constraints [], i.e. empty set, means impossible */ Set<String> getItemGroups (SimplePageItem item) { return getItemGroups(item, new HashSet<Long>()); } // find all groups allowed to use this item // intersect group directly associated with item // and groups allowed to use the page it's on Set<String> getItemGroups (SimplePageItem item, Set<Long>seen) { // null is a possible value. Because get returns null if something isn't // in the cache, use "null" for null. could also check whether it's in the cache // but documentation says this is expensive Object cached = cache.get(item.getId()); // Object cached = null; // for testing if (cached != null) { if (cached instanceof String) // "null" return null; return (Set<String>)cached; } Long itemId = (Long)item.getId(); if (seen.contains(itemId)) // loop, can't really get there return new HashSet<String>(); seen.add(itemId); // note that we're in progress, to stop infinte loop // if either has no restriction use the other // otherwise intersect them String itemGroupString = item.getGroups(); Set<String>itemGroups = null; if (itemGroupString != null && itemGroupString.length() > 0) itemGroups = new HashSet<String>(Arrays.asList(itemGroupString.split(","))); /// System.out.println("item " + item.getId() + "local groups " + itemGroups); Set<String> pageGroups = getPageGroups(item.getPageId(), seen); /// System.out.println("item " + item.getId() + "page groups " + pageGroups); if (itemGroups == null) itemGroups = pageGroups; else if (pageGroups == null) ; //nothing else itemGroups.retainAll(pageGroups); if (itemGroups == null) cache.put(item.getId(), "null", DEFAULT_EXPIRATION); else cache.put(item.getId(), itemGroups, DEFAULT_EXPIRATION); /// System.out.println("item " + item.getId() + " returned " + itemGroups); // no longer is progress seen.remove(itemId); return itemGroups; } // return all group allowed to access this page // find all items that point to the page and // take the union of groups allowed t use each Set<String> getPageGroups(long pageId, Set<Long>seen) { /// System.out.println("page " + pageId + " getgroups"); // if pageid is 0 this is a top level page. No further constraints if (pageId == 0) return null; Set<String> ret = new HashSet<String>(); // List of items with this page on it List<SimplePageItem> items = dao.findPageItemsBySakaiId(Long.toString(pageId)); /// System.out.print("page " + pageId + " called from items "); /// for (SimplePageItem item: items) { /// System.out.print(item.getId() + " "); /// } /// System.out.println(); for (SimplePageItem item: items) { // union all their groups Set<String> pageGroups = getItemGroups(item, seen); /// System.out.println("page " + pageId + "item " + item.getId() + " " + pageGroups); // except if we find one that's unconstrained, the final result is thta if (pageGroups == null) return null; ret.addAll(pageGroups); } /// System.out.println("page " + pageId + " returns " + ret); return ret; } public boolean isAssignmentVisible(String id, String userId) { int i = id.lastIndexOf(":"); if (i < 0) return false; String prefix = id.substring(0, i); String itemNum = id.substring(i+1); // if it's a page rather than an item if ("lesson-builder".equals(prefix)) return isAssignmentPageVisible(itemNum, userId); SimplePageItem item = null; long itemId = 0; try { itemId = Long.parseLong(itemNum); item = dao.findItem(itemId); if (item == null) return false; } catch (Exception e){ return false; } // there are two things to check. One is whether the user is in the site. // the other is whether the item is grouped. If so, is the user in that group SimplePage page = dao.getPage(item.getPageId()); String siteId = page.getSiteId(); String ref = "/site/" + siteId; boolean visible = securityService.unlock(userId, SimplePage.PERMISSION_LESSONBUILDER_READ, ref); if (!visible) return false; // can access the site. If not grouped, we're done. Set<String> groupIds = getItemGroups(item); if (groupIds == null) return true; ArrayList<String> groups = new ArrayList<String>(); for (String groupId: groupIds) groups.add("/site/" + siteId + "/group/" + groupId); List<AuthzGroup> matched = authzGroupService.getAuthzUserGroupIds(groups, userId); return (matched.size() > 0); } public boolean isAssignmentPageVisible(String pageNum, String userId) { SimplePage page = null; long pageId = 0; try { pageId = Long.parseLong(pageNum); page = dao.getPage(pageId); if (page == null) return false; } catch (Exception e){ return false; } // there are two things to check. One is whether the user is in the site. // the other is whether the item is grouped. If so, is the user in that group String siteId = page.getSiteId(); String ref = "/site/" + siteId; boolean visible = securityService.unlock(userId, SimplePage.PERMISSION_LESSONBUILDER_READ, ref); if (!visible) return false; // can access the site. If not grouped, we're done. Set<String> groupIds = getPageGroups(pageId, new HashSet<Long>()); if (groupIds == null) return true; ArrayList<String> groups = new ArrayList<String>(); for (String groupId: groupIds) groups.add("/site/" + siteId + "/group/" + groupId); List<AuthzGroup> matched = authzGroupService.getAuthzUserGroupIds(groups, userId); return (matched.size() > 0); } public List<String> getExternalAssignmentsForCurrentUser(String gradebookUid) { List<String> ret = new ArrayList<String>(); String ref = "/site/" + gradebookUid; boolean visible = securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_READ, ref); if (!visible) return ret; String userId = UserDirectoryService.getCurrentUser().getId(); List<SimplePageItem> externalItems = dao.findGradebookItems(gradebookUid); for (SimplePageItem item : externalItems) { Set<String> groupIds = getItemGroups(item); /// System.out.println("item " + item.getId() + " " + groupIds); if (groupIds == null) { if (item.getGradebookId() != null) ret.add(item.getGradebookId()); if (item.getAltGradebook() != null) ret.add(item.getAltGradebook()); } else { ArrayList<String> groups = new ArrayList<String>(); for (String groupId: groupIds) { groups.add("/site/" + gradebookUid + "/group/" + groupId); } List<AuthzGroup> matched = authzGroupService.getAuthzUserGroupIds(groups, userId); if (matched.size() > 0) { if (item.getGradebookId() != null) ret.add(item.getGradebookId()); if (item.getAltGradebook() != null) ret.add(item.getAltGradebook()); } } } List<SimplePage> externalPages = dao.findGradebookPages(gradebookUid); for (SimplePage page : externalPages) { Set<String> groupIds = getPageGroups(page.getPageId(), new HashSet<Long>()); /// System.out.println("item " + item.getId() + " " + groupIds); if (groupIds == null) { if (page.getGradebookPoints() != null) ret.add("lesson-builder:" + page.getPageId()); } else { ArrayList<String> groups = new ArrayList<String>(); for (String groupId: groupIds) { groups.add("/site/" + gradebookUid + "/group/" + groupId); } List<AuthzGroup> matched = authzGroupService.getAuthzUserGroupIds(groups, userId); if (matched.size() > 0) { if (page.getGradebookPoints() != null) ret.add("lesson-builder:" + page.getPageId()); } } } // list of items we have modified the group membership for. We have to override the // value returned by the tool itself Map<String, ArrayList<String>> otherTools = getExternalAssigns(gradebookUid); for (Map.Entry<String, ArrayList<String>> entry: otherTools.entrySet()) { if (entry.getValue() == null) ret.add(entry.getKey()); else { List<AuthzGroup> matched = authzGroupService.getAuthzUserGroupIds(entry.getValue(), userId); if (matched.size() > 0) ret.add(entry.getKey()); } } return ret; } public Map<String, List<String>> getAllExternalAssignments(String gradebookUid, Collection<String> studentIds) { Map<String,List<String>> allExternals = new HashMap<String, List<String>>(); String ref = "/site/" + gradebookUid; List<User> allowedUsers = securityService.unlockUsers(SimplePage.PERMISSION_LESSONBUILDER_READ, ref); if (allowedUsers.size() < 1) return allExternals; // no users allowed, nothing to do List<String> allowedIds = new ArrayList<String>(); for (User user: allowedUsers) allowedIds.add(user.getId()); // remove any user without lesson builder read in the site studentIds.retainAll(allowedIds); for (String studentId : studentIds) { allExternals.put(studentId, new ArrayList<String>()); } List<SimplePageItem> externalItems = dao.findGradebookItems(gradebookUid); for (SimplePageItem item: externalItems) { Set<String> groupIds = getItemGroups(item); if (groupIds == null) { // no restriction add to all users for (String userId : studentIds) if (allExternals.containsKey(userId)) { if (item.getGradebookId() != null) allExternals.get(userId).add(item.getGradebookId()); if (item.getAltGradebook() != null) allExternals.get(userId).add(item.getAltGradebook()); } } else { // restricted to group Set<String> groups = new HashSet<String>(); for (String groupId: groupIds) groups.add("/site/" + gradebookUid + "/group/" + groupId); // see if anyone on our list is in one of the groups // this call is new, but this code is only needed for 2.9.1 and later Set<String> okUsers = new HashSet<String>(authzGroupService.getAuthzUsersInGroups(groups)); okUsers.retainAll(studentIds); for (String userId : okUsers) if (allExternals.containsKey(userId)) { if (item.getGradebookId() != null) allExternals.get(userId).add(item.getGradebookId()); if (item.getAltGradebook() != null) allExternals.get(userId).add(item.getAltGradebook()); } } } List<SimplePage> externalPages = dao.findGradebookPages(gradebookUid); for (SimplePage page: externalPages) { Set<String> groupIds = getPageGroups(page.getPageId(),new HashSet<Long>()); if (groupIds == null) { // no restriction add to all users for (String userId : studentIds) if (allExternals.containsKey(userId)) { if (page.getGradebookPoints() != null) allExternals.get(userId).add("lesson-builder:" + page.getPageId()); } } else { // restricted to group Set<String> groups = new HashSet<String>(); for (String groupId: groupIds) groups.add("/site/" + gradebookUid + "/group/" + groupId); // see if anyone on our list is in one of the groups // this call is new, but this code is only needed for 2.9.1 and later Set<String> okUsers = new HashSet<String>(authzGroupService.getAuthzUsersInGroups(groups)); okUsers.retainAll(studentIds); for (String userId : okUsers) if (allExternals.containsKey(userId)) { if (page.getGradebookPoints() != null) allExternals.get(userId).add("lesson-builder:" + page.getPageId()); } } } // now handle other tools. If we modified their groups, we need to find the original groups // and return the users that match those groups // find list of items we've modified // map of external ID to list of original groups for that item Map<String, ArrayList<String>> otherTools = getExternalAssigns(gradebookUid); // for each externalId for (Map.Entry<String, ArrayList<String>> entry: otherTools.entrySet()) { String externalId = entry.getKey(); // if no group restriction if (entry.getValue() == null) { // add this item to all students for (String userId : studentIds) if (allExternals.containsKey(userId)) allExternals.get(userId).add(externalId); } else { // otherwise find users that are in the groups Set<String> okUsers = new HashSet<String>(authzGroupService.getAuthzUsersInGroups(new HashSet<String>(entry.getValue()))); okUsers.retainAll(studentIds); // and add this item just to them for (String u : okUsers) if (allExternals.containsKey(u)) { allExternals.get(u).add(externalId); } } } return allExternals; } // for the moment just our own assignments public List<String> getAllExternalAssignments(String gradebookUid) { List<String> ret = new ArrayList<String>(); List<SimplePageItem> externalItems = dao.findGradebookItems(gradebookUid); for (SimplePageItem item: externalItems) { if (item.getGradebookId() != null) ret.add(item.getGradebookId()); if (item.getAltGradebook() != null) ret.add(item.getAltGradebook()); } return ret; } // return list of items where we've replaced the group with our own // returns map externalId -> group list public Map<String,ArrayList<String>> getExternalAssigns(String gradebookUid) { Map<String,ArrayList<String>> ret = new HashMap<String,ArrayList<String>>(); // should this use the Dao? Not clear that it makes sense in the Lessons dao. // I don't see a good approach in gradebook to do this. We could fetch all assignments // and check externally maintained, but this is performace critical, so I hate to do that. // find the external items in the gradebook. Unfortunately we can have // items in the lesson_builder_groups table that aren't in the gradebook String sql = "select b.external_id from GB_GRADEBOOK_T a, GB_GRADABLE_OBJECT_T b where a.gradebook_uid=? and a.id=b.GRADEBOOK_ID and b.EXTERNALLY_MAINTAINED=1"; Object[] fields = new Object[1]; fields[0] = gradebookUid; List<String> externalIds = SqlService.dbRead(sql, fields, null); if (externalIds == null) return null; Set<String> externalIdSet = new HashSet<String>(externalIds); // map sakaiId to group list as text Map<String,String> externals = dao.getExternalAssigns(gradebookUid); for (Map.Entry<String,String> entry: externals.entrySet()) { String sakaiId = entry.getKey(); String externalId = null; // only handle item types for which we actually hack on the group membership // internal LB items are handled elsewhere, so this is just assignments and Samigo // assignment 2 and Forums use gradebook items that aren't external, so group // membership isn't relevant. We have to map from format of sakaiId to format // of gradebook's external ID. GB uses just the item number for Samigo, and // the full reference for Assignment if (sakaiId.startsWith("/sam_pub/")) externalId = sakaiId.substring("/sam_pub/".length()); else if (sakaiId.startsWith("/assignment/")) externalId = "/assignment/a/" + gradebookUid + "/" + sakaiId.substring("/assignment/".length()); // sam and assignment is all we deal with, so anything else // in new format we skip. Following is old format sakaiid's where // the ID numbers were included with no type information else if (sakaiId.startsWith("/")) continue; else if (sakaiId.indexOf("-") >= 0) // old format assignment externalId = "/assignment/a/" + gradebookUid + "/" + sakaiId; else externalId = sakaiId; // old format Samigo // in our groups table but not in gradebook, don't need it if (!externalIdSet.contains(externalId)) continue; // have external ID, get group list ArrayList<String>groups = null; String groupString = entry.getValue(); if (groupString != null && !groupString.equals("")) { groups = new ArrayList<String>(); String [] groupArray = groupString.split(","); for (String groupId: groupArray) { groups.add("/site/" + gradebookUid + "/group/" + groupId); } } ret.put(externalId, groups); } return ret; } public void setGradebookExternalAssessmentService(GradebookExternalAssessmentService geaService) { this.geaService = geaService; } public GradebookExternalAssessmentService getGradebookExternalAssessmentService() { return geaService; } public void setAuthzGroupService(AuthzGroupService authzGroupService) { this.authzGroupService = authzGroupService; } public AuthzGroupService getAuthzGroupService() { return authzGroupService; } public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } public SecurityService getSecurityService() { return securityService; } public void setSimplePageToolDao(SimplePageToolDao s) { dao = s; } public void setMemoryService(MemoryService m) { memoryService = m; } }