/**********************************************************************************
* $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;
}
}