/********************************************************************************** * $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.TreeSet; import java.util.Comparator; import java.util.Date; import java.util.Map; import java.util.Iterator; import java.util.Properties; import java.net.URLEncoder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.lessonbuildertool.service.LessonSubmission; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean; import org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean.UrlItem; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.cover.ToolManager; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.site.api.ToolConfiguration; 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.lti.api.LTIService; // import org.sakaiproject.lti.impl.DBLTIService; // HACK /** * 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 BltiEntity implements LessonEntity, BltiInterface { private static Log log = LogFactory.getLog(BltiEntity.class); private static Cache bltiCache = null; protected static final int DEFAULT_EXPIRATION = 10 * 60; private SimplePageBean simplePageBean; protected static LTIService ltiService = null; public void setSimplePageBean(SimplePageBean simplePageBean) { this.simplePageBean = simplePageBean; } private LessonEntity nextEntity = null; public void setNextEntity(LessonEntity e) { nextEntity = e; } public LessonEntity getNextEntity() { return nextEntity; } static MemoryService memoryService = null; public void setMemoryService(MemoryService m) { memoryService = m; } static MessageLocator messageLocator = null; public void setMessageLocator(MessageLocator m) { messageLocator = m; } static String returnUrl = null; public void setReturnUrl(String m) { returnUrl = m; } public void init () { log.info("init()"); bltiCache = memoryService .newCache("org.sakaiproject.lessonbuildertool.service.BltiEntity.cache"); /* Hack to avoid a restart to get a new version of DBLTIService if ( ltiService == null ) { ltiService = (LTIService) new DBLTIService(); ((org.sakaiproject.lti.impl.DBLTIService) ltiService).setAutoDdl("true"); ((org.sakaiproject.lti.impl.DBLTIService) ltiService).init(); } */ if ( ltiService == null ) { Object service = ComponentManager.get("org.sakaiproject.lti.api.LTIService"); if (service == null) { log.info("can't find LTI Service -- disabling LTI support"); return; } ltiService = (LTIService)service; log.info("LTI initialized"); } } public void destroy() { bltiCache.destroy(); bltiCache = 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 public boolean servicePresent() { return ltiService != null; } protected BltiEntity() { } protected BltiEntity(int type, String id) { this.type = type; this.id = id; } public String getToolId() { return "sakai.blti"; } // the underlying object, something Sakaiish protected String id; protected int type; // not required fields. If we need to look up // the actual objects, lets us cache them protected Map<String,Object> content; protected Map<String,Object> tool; /* public Blti getBlti(String ref, boolean nocache) { Blti ret = (Blti)bltiCache.get(ref); if (!nocache && ret != null) return ret; try { // ret = BltiService.getBlti(ref); } catch (Exception e) { ret = null; } if (ret != null) bltiCache.put(ref, ret, DEFAULT_EXPIRATION); return ret; } */ // type of the underlying object public int getType() { return type; } public int getLevel() { return 0; } public int getTypeOfGrade() { return 1; } // hack for forums. not used for assessments, so always ok public boolean isUsable() { return true; } public String getReference() { return "/" + BLTI + "/" + id; } public List<LessonEntity> getEntitiesInSite() { return getEntitiesInSite(null); } // find topics in site, but organized by forum public List<LessonEntity> getEntitiesInSite(SimplePageBean bean) { List<LessonEntity> ret = new ArrayList<LessonEntity>(); if (ltiService == null) return ret; List<Map<String,Object>> contents = ltiService.getContents(null,null,0,0); for (Map<String, Object> content : contents ) { Long id = getLong(content.get(LTIService.LTI_ID)); if ( id == -1 ) continue; BltiEntity entity = new BltiEntity(TYPE_BLTI, id.toString()); entity.content = content; ret.add(entity); } return ret; } public LessonEntity getEntity(String ref, SimplePageBean o) { return getEntity(ref); } public LessonEntity getEntity(String ref) { int i = ref.indexOf("/",1); String typeString = ref.substring(1, i); String idString = ref.substring(i+1); String id = ""; try { id = idString; } catch (Exception ignore) { return null; } if (typeString.equals(BLTI)) { return new BltiEntity(TYPE_BLTI, id); } else if (nextEntity != null) { // in case we chain to a different implementation. Not likely for BLTI return nextEntity.getEntity(ref); } else return null; } protected void loadContent() { if ( content != null ) return; if ( id == null ) return; // Likely a failure if ( ltiService == null) return; // not basiclti or old Long key = getLong(id); content = ltiService.getContent(key); if ( content == null ) return; Long toolKey = getLongNull(content.get("tool_id")); if (toolKey != null ) tool = ltiService.getTool(toolKey); } // properties of entities public String getTitle() { loadContent(); if ( content == null ) return null; return (String) content.get(LTIService.LTI_TITLE); } private String getErrorUrl() { return "javascript:document.write('" + messageLocator.getMessage("simplepage.format.item_removed").replace("'", "\\'") + "')"; } // TODO: Concern regarding the lack of the returnUrl when this is called public String getUrl() { loadContent(); // If I return null here, it appears that I cause an NPE in LB if ( content == null ) return getErrorUrl(); String ret = (String) content.get("launch_url"); if ( ltiService != null && tool != null && ltiService.isMaintain() && LTIService.LTI_SECRET_INCOMPLETE.equals((String) tool.get(LTIService.LTI_SECRET)) && LTIService.LTI_SECRET_INCOMPLETE.equals((String) tool.get(LTIService.LTI_CONSUMERKEY)) ) { String toolId = getCurrentTool("sakai.siteinfo"); if ( toolId != null ) { ret = editItemUrl(toolId); return ret; } } ret = ServerConfigurationService.getServerUrl() + ret; return ret; } public Date getDueDate() { return null; } // 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 // access control public boolean addEntityControl(String siteId, String groupId) throws IOException { // not used for BLTI, control is done entirely within LB 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 false; } public LessonSubmission getSubmission(String userId) { // students don't have submissions to BLTI return null; } // 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) { return 0; } // 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>(); String toolId = bean.getCurrentTool("sakai.siteinfo"); if ( ltiService == null || toolId == null || returnUrl == null ) return list; // Retrieve all tools List<Map<String,Object>> tools = ltiService.getTools(null,null,0,0); for ( Map<String,Object> tool : tools ) { String url = ServerConfigurationService.getToolUrl() + "/" + toolId + "/sakai.basiclti.admin.helper.helper?panel=ContentConfig&tool_id=" + tool.get(LTIService.LTI_ID) + "&returnUrl=" + URLEncoder.encode(returnUrl); list.add(new UrlItem(url, (String) tool.get(LTIService.LTI_TITLE))); } String url = ServerConfigurationService.getToolUrl() + "/" + toolId + "/sakai.basiclti.admin.helper.helper?panel=Main" + "&returnUrl=" + URLEncoder.encode(returnUrl); list.add(new UrlItem(url, messageLocator.getMessage("simplepage.create_blti"))); return list; } public boolean isPopUp() { loadContent(); if (content == null) return false; Long newPage = getLong(content.get(LTIService.LTI_NEWPAGE)); return (newPage == 1) ; } public int frameSize() { loadContent(); if ( content == null ) return -1; Long newPage = getLong(content.get(LTIService.LTI_FRAMEHEIGHT)); return newPage.intValue(); } // 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 toolId = bean.getCurrentTool("sakai.siteinfo"); if ( toolId == null ) return null; return editItemUrl(toolId); } public String editItemUrl(String toolId) { if ( toolId == null ) return null; loadContent(); if (content == null) return null; String url = ServerConfigurationService.getToolUrl() + "/" + toolId + "/sakai.basiclti.admin.helper.helper?panel=ContentConfig&id=" + content.get(LTIService.LTI_ID); if ( returnUrl != null ) { url = url + "&returnUrl=" + URLEncoder.encode(returnUrl); } else { url = url + "&returnUrl=about:blank"; } return url; } // 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() { loadContent(); return content != 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) { // done entirely within LB, this item type is not group-aware return null; } // set the item to be accessible only to the specific groups. // null to make it accessible to the whole site public void setGroups(Collection<String> groups) { // not group aware } public String doImportTool(String launchUrl, String bltiTitle, String strXml, String custom) { if ( ltiService == null ) return null; Map<String,Object> theTool = null; List<Map<String,Object>> tools = ltiService.getTools(null,null,0,0); for ( Map<String,Object> tool : tools ) { String toolLaunch = (String) tool.get(LTIService.LTI_LAUNCH); if ( toolLaunch.equals(launchUrl) ) { theTool = tool; break; } } if ( theTool == null ) { Properties props = new Properties (); props.setProperty(LTIService.LTI_LAUNCH,launchUrl); props.setProperty(LTIService.LTI_TITLE, bltiTitle); props.setProperty(LTIService.LTI_CONSUMERKEY, LTIService.LTI_SECRET_INCOMPLETE); props.setProperty(LTIService.LTI_SECRET, LTIService.LTI_SECRET_INCOMPLETE); props.setProperty(LTIService.LTI_ALLOWCUSTOM, "1"); props.setProperty(LTIService.LTI_XMLIMPORT,strXml); if (custom != null) props.setProperty(LTIService.LTI_CUSTOM, custom); Object result = ltiService.insertTool(props); if ( result instanceof String ) { System.out.println("Could not insert tool - "+result); } if ( result instanceof Long ) theTool = ltiService.getTool((Long) result); } Map<String,Object> theContent = null; Long contentKey = null; if ( theTool != null ) { Properties props = new Properties (); props.setProperty(LTIService.LTI_TOOL_ID,getLong(theTool.get(LTIService.LTI_ID)).toString()); props.setProperty(LTIService.LTI_TITLE, bltiTitle); props.setProperty(LTIService.LTI_LAUNCH,launchUrl); props.setProperty(LTIService.LTI_XMLIMPORT,strXml); if ( custom != null ) props.setProperty(LTIService.LTI_CUSTOM,custom); Object result = ltiService.insertContent(props); if ( result instanceof String ) { System.out.println("Could not insert content - "+result); } else { System.out.println("Adding LTI tool "+result); } if ( result instanceof Long ) theContent = ltiService.getContent((Long) result); } String sakaiId = null; if ( theContent != null ) { sakaiId = "/blti/" + theContent.get(LTIService.LTI_ID); } return sakaiId; } // TODO: Could we get simplePageBean populated here and not build out own get public String getCurrentTool(String commonToolId) { try { String currentSiteId = ToolManager.getCurrentPlacement().getContext(); Site site = SiteService.getSite(currentSiteId); ToolConfiguration toolConfig = site.getToolForCommonId(commonToolId); if (toolConfig == null) return null; return toolConfig.getId(); } catch (Exception e) { return null; } } public Long getLong(Object key) { Long retval = getLongNull(key); if (retval != null) return retval; return new Long(-1); } public Long getLongNull(Object key) { if (key == null) return null; if (key instanceof Number) return new Long(((Number) key).longValue()); if (key instanceof String) { try { return new Long((String) key); } catch (Exception e) { return null; } } return null; } public String getObjectId(){ return null; } public String findObject(String objectid, Map<String,String>objectMap, String siteid) { if (nextEntity != null) return nextEntity.findObject(objectid, objectMap, siteid); return null; } public String getSiteId() { loadContent(); if ( content == null ) return null; return (String) content.get(LTIService.LTI_SITE_ID); } }