/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/event/impl/BaseLearningResourceStoreService.java $ * $Id: BaseLearningResourceStoreService.java 124829 2013-05-22 12:55:35Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2008 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.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.event.impl; import java.util.HashSet; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.event.api.Event; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.LearningResourceStoreProvider; import org.sakaiproject.event.api.LearningResourceStoreService; import org.sakaiproject.event.api.LearningResourceStoreService.LRS_Verb.SAKAI_VERB; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserNotDefinedException; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * Core implementation of the LRS integration * This will basically just reroute calls over to the set of known {@link LearningResourceStoreProvider}. * It also does basic config handling (around enabling/disabling the overall processing) and filtering handled statement origins. * * Configuration: * 1) Enable LRS processing * Default: false * lrs.enabled=true * 2) Enabled statement origin filters * Default: No filters (all statements processed) * lrs.origins.filter=tool1,tool2,tool3 * * @author Aaron Zeckoski (azeckoski @ unicon.net) (azeckoski @ vt.edu) */ //@Aspect public class BaseLearningResourceStoreService implements LearningResourceStoreService, ApplicationContextAware { private static final String ORIGIN_SAKAI_SYSTEM = "sakai.system"; private static final String ORIGIN_SAKAI_CONTENT = "sakai.resources"; private static final Log log = LogFactory.getLog(BaseLearningResourceStoreService.class); /** * Stores the complete set of known LRSP providers (from the Spring AC or registered manually) */ private ConcurrentHashMap<String, LearningResourceStoreProvider> providers; /** * Contains a timestamp of the last time we warned that there are no providers to handle processing */ private long noProvidersWarningTS = 0; /** * Stores the complete set of origin filters for the LRS service, * Anything with an origin that matches the ones in this set will be blocked from being processed */ private HashSet<String> originFilters; /** * Allows us to be notified of all incoming local events */ private ExperienceObserver experienceObserver; public void init() { providers = new ConcurrentHashMap<String, LearningResourceStoreProvider>(); // search for known providers if (isEnabled() && applicationContext != null) { @SuppressWarnings("unchecked") Map<String, LearningResourceStoreProvider> beans = applicationContext.getBeansOfType(LearningResourceStoreProvider.class); for (LearningResourceStoreProvider lrsp : beans.values()) { if (lrsp != null) { // should not be null but this avoids killing everything if it is registerProvider(lrsp); } } log.info("LRS Registered "+beans.size()+" LearningResourceStoreProviders from the Spring AC during service INIT"); } else { log.info("LRS did not search for existing LearningResourceStoreProviders in the system (ac="+applicationContext+", enabled="+isEnabled()+")"); } if (isEnabled() && serverConfigurationService != null) { String[] filters = serverConfigurationService.getStrings("lrs.origins.filter"); if (filters == null || filters.length == 0) { log.info("LRS filters are not configured: All statements will be passed through to the LRS"); } else { originFilters = new HashSet<String>(filters.length); for (int i = 0; i < filters.length; i++) { if (filters[i] != null) { originFilters.add(filters[i]); } } log.info("LRS found "+originFilters.size()+" origin filters: "+originFilters); } } if (isEnabled() && eventTrackingService != null) { this.experienceObserver = new ExperienceObserver(this); eventTrackingService.addLocalObserver(this.experienceObserver); log.info("LRS registered local event tracking observer"); } log.info("LRS INIT: enabled="+isEnabled()); } public void destroy() { if (providers != null) { providers.clear(); } originFilters = null; providers = null; if (experienceObserver != null && eventTrackingService != null) { eventTrackingService.deleteObserver(experienceObserver); } experienceObserver = null; log.info("LRS DESTROY"); } /* (non-Javadoc) * @see org.sakaiproject.event.api.LearningResourceStoreService#registerStatement(org.sakaiproject.event.api.LearningResourceStoreService.LRS_Statement, java.lang.String) */ public void registerStatement(LRS_Statement statement, String origin) { if (statement == null) { log.error("LRS registerStatement call INVALID, statement is null and must not be"); //throw new IllegalArgumentException("statement must be set"); } else if (isEnabled()) { if (providers == null || providers.isEmpty()) { if (noProvidersWarningTS < (System.currentTimeMillis() - 86400000)) { // check if we already warned in the last 24 hours noProvidersWarningTS = System.currentTimeMillis(); log.warn("LRS statement from ("+origin+") skipped because there are no providers to process it: "+statement); } } else { // filter out certain tools and statement origins boolean skip = false; if (originFilters != null && !originFilters.isEmpty()) { origin = StringUtils.trimToNull(origin); if (origin != null && originFilters.contains(origin)) { if (log.isDebugEnabled()) log.debug("LRS statement skipped because origin ("+origin+") matches the originFilter"); skip = true; } } if (!skip) { // validate the statement boolean valid = false; if (statement.isPopulated() && statement.getActor() != null && statement.getVerb() != null && statement.getObject() != null) { valid = true; } else if (statement.getRawMap() != null && !statement.getRawMap().isEmpty()) { valid = true; } else if (statement.getRawJSON() != null && !StringUtils.isNotBlank(statement.getRawJSON())) { valid = true; } if (valid) { // process this statement if (log.isDebugEnabled()) log.debug("LRS statement being processed, origin="+origin+", statement="+statement); for (LearningResourceStoreProvider lrsp : providers.values()) { // run the statement processing in a new thread String threadName = "LRS_"+lrsp.getID(); Thread t = new Thread(new RunStatementThread(lrsp, statement), threadName); // each provider has it's own thread t.setDaemon(true); // allow this thread to be killed when the JVM is shutdown t.start(); } } else { log.warn("Invalid statment registered, statement will not be processed: "+statement); } } else { if (log.isDebugEnabled()) log.debug("LRS statement being skipped, origin="+origin+", statement="+statement); } } } } /** * internal class to support threaded execution of statements processing */ private static class RunStatementThread implements Runnable { final LearningResourceStoreProvider lrsp; final LRS_Statement statement; public RunStatementThread(LearningResourceStoreProvider lrsp, LRS_Statement statement) { this.lrsp = lrsp; this.statement = statement; } @Override public void run() { try { lrsp.handleStatement(statement); } catch (Exception e) { log.error("LRS Failure running LRS statement in provider ("+lrsp.getID()+"): statement=("+statement+"): "+e, e); } } }; /* (non-Javadoc) * @see org.sakaiproject.event.api.LearningResourceStoreService#isEnabled() */ public boolean isEnabled() { boolean enabled = false; if (serverConfigurationService != null) { enabled = serverConfigurationService.getBoolean("lrs.enabled", enabled); } return enabled; } /* (non-Javadoc) * @see org.sakaiproject.event.api.LearningResourceStoreService#registerProvider(org.sakaiproject.event.api.LearningResourceStoreProvider) */ public boolean registerProvider(LearningResourceStoreProvider provider) { if (provider == null) { throw new IllegalArgumentException("LRS provider must not be null"); } return providers.put(provider.getID(), provider) != null; } /* AOP processing of events * NOTE: this probably won't end up working - we might need to just use the service directly * mostly because the types of the objects (like Assignment) cannot be pulled into the * kernel because it would make the kernel depend on those projects, * but I think we will need access to the values in those objects and getting them * all via reflection is going to be a too much effort -AZ * * Alos, for runtime weaving (load-time) we have to add this to spring and put aspect j into shared * <aop:aspectj-autoproxy/> * <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> */ /* * Leaving this in so we don't forget that it was attempted * @Before("execution(* org.sakaiproject.event.impl.*Service.init())") public void sampleBeforeInit() { log.info("AOP Before init()"); } @AfterReturning("execution(* org.sakaiproject.event.impl.*Service.init(..))") public void sampleAfterInit() { log.info("AOP After init()"); } @Around("execution(* *.setUsageSessionServiceSql(String)) && args(vendor)") public void sampleAround(ProceedingJoinPoint thisJoinPoint, String vendor) throws Throwable { log.info("AOP Around (before) setUsageSessionServiceSql("+vendor+")"); thisJoinPoint.proceed(new Object[] {vendor}); log.info("AOP Around (after) setUsageSessionServiceSql("+vendor+")"); } */ // EVENT conversion to statements private static class ExperienceObserver implements Observer { final BaseLearningResourceStoreService lrss; public ExperienceObserver(BaseLearningResourceStoreService lrss) { this.lrss = lrss; } @Override public void update(Observable observable, Object object) { if (object != null && object instanceof Event) { Event event = (Event) object; // convert event into origin String origin = this.lrss.getEventOrigin(event); // convert event into statement when possible LRS_Statement statement = this.lrss.getEventStatement(event); if (statement != null) { this.lrss.registerStatement(statement, origin); } } } } /** * Convenience method to turn events into statements, * this can only work for simple events where there is little student interaction * @param event an Event * @return a statement if one can be formed OR null if not */ private LRS_Statement getEventStatement(Event event) { LRS_Statement statement; try { LRS_Verb verb = getEventVerb(event); if (verb != null) { LRS_Object object = getEventObject(event); if (object != null) { LRS_Actor actor = getEventActor(event); statement = new LRS_Statement(actor, verb, object); LRS_Context c = getEventContext(event); if (c != null) { statement.setContext(c); } } else { statement = null; } } else { statement = null; } } catch (Exception e) { if (log.isDebugEnabled()) log.debug("LRS Unable to convert event ("+event+") into statement: "+e); statement = null; } return statement; } /* (non-Javadoc) * @see org.sakaiproject.event.api.LearningResourceStoreService#getEventActor(org.sakaiproject.event.api.Event) */ public LRS_Actor getEventActor(Event event) { LRS_Actor actor = null; User user = null; if (event.getUserId() != null) { try { user = this.userDirectoryService.getUser(event.getUserId()); } catch (UserNotDefinedException e) { user = null; } } if (user == null && event.getSessionId() != null) { Session session = this.sessionManager.getSession(event.getSessionId()); if (session != null) { try { user = this.userDirectoryService.getUser(session.getUserId()); } catch (UserNotDefinedException e) { user = null; } } } if (user != null) { String actorEmail; if (StringUtils.isNotEmpty(user.getEmail())) { actorEmail = user.getEmail(); } else { // no email set - make up something like one String server = serverConfigurationService.getServerName(); if ("localhost".equals(server)) { server = "tincanapi.dev.sakaiproject.org"; } else { server = serverConfigurationService.getServerId()+"."+server; } actorEmail = user.getId()+"@"+server; if (log.isDebugEnabled()) log.debug("LRS Actor: No email set for user ("+user.getId()+"), using generated one: "+actorEmail); } actor = new LRS_Actor(actorEmail); if (StringUtils.isNotEmpty(user.getDisplayName())) { actor.setName(user.getDisplayName()); } } return actor; } /** * @param event * @return a valid context for the event (based on the site/course) OR null if one cannot be determined */ private LRS_Context getEventContext(Event event) { LRS_Context context = null; if (event != null && event.getContext() != null) { String eventContext = event.getContext(); String e = StringUtils.lowerCase(event.getEvent()); // NOTE: wiki puts /site/ in front of the context, others are just the site_id if (StringUtils.startsWith(e, "wiki")) { eventContext = StringUtils.replace(eventContext, "/site/", ""); } // the site is the parent for all event activities context = new LRS_Context("parent", serverConfigurationService.getPortalUrl()+"/site/"+eventContext); } return context; } private LRS_Verb getEventVerb(Event event) { LRS_Verb verb = null; if (event != null) { String e = StringUtils.lowerCase(event.getEvent()); if ("user.login".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.initialized); } else if ("user.logout".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.exited); } else if ("annc.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("calendar.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("chat.new".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.responded); } else if ("chat.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("content.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.interacted); } else if ("content.new".equals(e) || "content.revise".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.shared); } else if ("gradebook.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("lessonbuilder.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("news.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("podcast.read".equals(e) || "podcast.read.public".equals(e) || "podcast.read.site".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("syllabus.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("webcontent.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } else if ("wiki.new".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.initialized); } else if ("wiki.revise".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.shared); } else if ("wiki.read".equals(e)) { verb = new LRS_Verb(SAKAI_VERB.experienced); } } return verb; } private LRS_Object getEventObject(Event event) { LRS_Object object = null; if (event != null) { String e = StringUtils.lowerCase(event.getEvent()); /* * NOTE: use the following terms "view", "add", "edit", "delete" */ if ("user.login".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl(), "session-started"); } else if ("user.logout".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl()+"/logout", "session-ended"); } else if ("annc.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-announcement"); } else if ("calendar.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-calendar"); } else if ("chat.new".equals(e) || "chat.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-chats"); } else if ("content.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getAccessUrl() + event.getResource(), "view-resource"); } else if ("content.new".equals(e)) { object = new LRS_Object(serverConfigurationService.getAccessUrl() + event.getResource(), "add-resource"); } else if ("content.revise".equals(e)) { object = new LRS_Object(serverConfigurationService.getAccessUrl() + event.getResource(), "edit-resource"); } else if ("gradebook.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-grades"); } else if ("lessonbuilder.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-lesson"); } else if ("news.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-news"); } else if ("podcast.read".equals(e) || "podcast.read.public".equals(e) || "podcast.read.site".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-podcast"); } else if ("syllabus.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-syllabus"); } else if ("webcontent.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-web-content"); } else if ("wiki.new".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "add-wiki-page"); } else if ("wiki.revise".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "edit-wiki-page"); } else if ("wiki.read".equals(e)) { object = new LRS_Object(serverConfigurationService.getPortalUrl() + event.getResource(), "view-wiki-page"); } } return object; } private String getEventOrigin(Event event) { String origin = null; if (event != null) { String e = StringUtils.lowerCase(event.getEvent()); if ("user.login".equals(e) || "user.logout".equals(e)) { origin = ORIGIN_SAKAI_SYSTEM; } else if ("annc.read".equals(e)) { origin = "announcement"; } else if ("calendar.read".equals(e)) { origin = "calendar"; } else if ("chat.new".equals(e) || "chat.read".equals(e)) { origin = "chat"; } else if ("content.read".equals(e) || "content.new".equals(e) || "content.revise".equals(e)) { origin = ORIGIN_SAKAI_CONTENT; } else if ("gradebook.read".equals(e)) { origin = "gradebook"; } else if ("lessonbuilder.read".equals(e)) { origin = "lessonbuilder"; } else if ("news.read".equals(e)) { origin = "news"; } else if ("podcast.read".equals(e) || "podcast.read.public".equals(e) || "podcast.read.site".equals(e)) { origin = "podcast"; } else if ("syllabus.read".equals(e)) { origin = "syllabus"; } else if ("webcontent.read".equals(e)) { origin = "webcontent"; } else if ("wiki.new".equals(e) || "wiki.revise".equals(e) || "wiki.read".equals(e)) { origin = "rwiki"; } else { origin = e; } } return origin; } // INJECTION ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } EventTrackingService eventTrackingService; public void setEventTrackingService(EventTrackingService eventTrackingService) { this.eventTrackingService = eventTrackingService; } ServerConfigurationService serverConfigurationService; public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) { this.serverConfigurationService = serverConfigurationService; } SessionManager sessionManager; public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } UserDirectoryService userDirectoryService; public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } }