/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.upgrade.legacy;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.TypedQuery;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.PersistenceHelper;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.StringResourceableType;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.StringHelper;
import org.olat.core.util.cache.CacheWrapper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.SyncerCallback;
import org.olat.core.util.coordinate.SyncerExecutor;
import org.olat.core.util.event.GenericEventListener;
import org.olat.core.util.resource.OresHelper;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.assessment.AssessmentChangedEvent;
import org.olat.course.assessment.AssessmentLoggingAction;
import org.olat.course.assessment.AssessmentManager;
import org.olat.course.assessment.manager.EfficiencyStatementManager;
import org.olat.course.auditing.UserNodeAuditManager;
import org.olat.course.certificate.CertificateTemplate;
import org.olat.course.certificate.CertificatesManager;
import org.olat.course.certificate.model.CertificateInfos;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.properties.CoursePropertyManager;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.properties.Property;
import org.olat.repository.RepositoryEntry;
import org.olat.util.logging.activity.LoggingResourceable;
/**
* Description:<BR>
* The assessment manager is used by the assessable course nodes to store and
* retrieve user assessment data from the database. The assessment Manager
* should not be used directly from the controllers but only via the assessable
* course nodes interface.<BR>
* Exception are nodes that want to save or get node attempts variables for
* nodes that are not assessable nodes (e.g. questionnaire) <BR>
* This implementation will store its values using the property manager and has
* a cache built in for frequently used assessment data like score, passed and
* attempts variables.
* <P>
*
* the underlying cache is segmented as follows:
* 1.) by this class (=owner in singlevm, coowner in cluster mode)
* 2.) by course (so that e.g. deletion of a course removes all caches)
* 3.) by identity, for preloading and invalidating (e.g. a user entering a course will cause the identity's cache to be loaded)
*
* each cache only has -one- key, which is a hashmap with all the information (score,passed, etc) for the given user/course.
* the reason for this is that it must be possible to see a difference between a null value (key expired) and a value which corresponds to
* e.g. "this user has never attempted this test in this course". since only the concrete set, but not the possible set is known. (at least
* not in the database). so all keys of a given user/course will therefore expire together which also makes sense from a use point of view.
*
* Cache usage with e.g. the wiki: wikipages should be saved as separate keys, since no batch updates are needed for perf. reasons.
*
* reason for 3: preloading all data of all users of a course lasts up to 5 seconds and will waste memory.
* a user in a course only needs its own data. only when a tutor enters the assessment functionality, all data of all users is needed ->
* do a full load only then.
*
* TODO: e.g. IQTEST.onDelete(..) cleans all data without going over the assessmentmanager here. meaning that the cache has stale data in it.
* since coursenode.getIdent (partial key of this cache) is forever unique, then this doesn't really matter. - but it is rather unintended...
* point is that a node can save lots of data that have nothing to do with assessments
*
*
* @author Felix Jost
*/
public class NewCachePersistingAssessmentManager {
private static final OLog log = Tracing.createLoggerFor(NewCachePersistingAssessmentManager.class);
public static final String SCORE = "SCORE";
public static final String PASSED = "PASSED";
public static final String ATTEMPTS = "ATTEMPTS";
public static final String COMMENT = "COMMENT";
public static final String COACH_COMMENT = "COACH_COMMENT";
public static final String ASSESSMENT_ID = "ASSESSMENT_ID";
public final static String FULLY_ASSESSED = "FULLY_ASSESSED";
/**
* the key under which a hashmap is stored in a cachewrapper. we only use one key so that either all values of a user are there or none are there.
* (otherwise we cannot know whether a null value means expiration of cache or no-such-property-yet-for-user)
*/
//private static final String FULLUSERSET = "FULLUSERSET";
private static final String LAST_MODIFIED = "LAST_MODIFIED";
// Float and Integer are immutable objects, we can reuse them.
private static final Float FLOAT_ZERO = new Float(0);
private static final Integer INTEGER_ZERO = new Integer(0);
// the cache per assessment manager instance (=per course)
private CacheWrapper<NewCacheKey,HashMap<String, Serializable>> courseCache;
private OLATResourceable ores;
/**
* Private since singleton
*/
public NewCachePersistingAssessmentManager(ICourse course) {
this.ores = course;
//String cacheName = "Course@" + course.getResourceableId();
courseCache = CoordinatorManager.getInstance().getCoordinator().getCacher()
.getCache(AssessmentManager.class.getSimpleName(), "newpersisting");
}
public NewCachePersistingAssessmentManager(ICourse course, CacheWrapper<NewCacheKey,HashMap<String, Serializable>> cache) {
this.ores = course;
//String cacheName = "Course@" + course.getResourceableId();
courseCache = cache;
}
/**
* @param identity the identity for which to properties are to be loaded.
* if null, the properties of all identities (=all properties of this course)
* are loaded.
* @return
*/
private List<Property> loadPropertiesFor(List<Identity> identities) {
if(identities == null || identities.isEmpty()) return Collections.emptyList();
StringBuilder sb = new StringBuilder();
sb.append("from org.olat.properties.Property as p")
.append(" inner join fetch p.identity as ident ")
.append(" inner join fetch ident.user as user ")
.append(" where p.resourceTypeId = :restypeid and p.resourceTypeName = :restypename")
.append(" and p.name in ('")
.append(ATTEMPTS).append("','")
.append(SCORE).append("','")
.append(FULLY_ASSESSED).append("','")
.append(PASSED).append("','")
.append(ASSESSMENT_ID).append("','")
.append(COMMENT).append("','")
.append(COACH_COMMENT)
.append("')");
if (identities != null) {
sb.append(" and p.identity.key in (:id)");
}
TypedQuery<Property> query = DBFactory.getInstance().getCurrentEntityManager()
.createQuery(sb.toString(), Property.class)
.setParameter("restypename", ores.getResourceableTypeName())
.setParameter("restypeid", ores.getResourceableId());
if (identities != null) {
query.setParameter("id", PersistenceHelper.toKeys(identities));
}
return query.getResultList();
}
/**
* @see org.olat.course.assessment.AssessmentManager#preloadCache(org.olat.core.id.Identity)
*/
public void preloadCache(Identity identity) {
// triggers loading of data of the given user.
getOrLoadScorePassedAttemptsMap(identity, null, false);
}
public void preloadCache(List<Identity> identities) {
int count = 0;
int batch = 200;
Map<Identity, List<Property>> map = new HashMap<Identity, List<Property>>(201);
do {
int toIndex = Math.min(count + batch, identities.size());
List<Identity> toLoad = identities.subList(count, toIndex);
List<Property> allProperties = loadPropertiesFor(toLoad);
map.clear();
for(Property prop:allProperties) {
if(!map.containsKey(prop.getIdentity())) {
map.put(prop.getIdentity(), new ArrayList<Property>());
}
map.get(prop.getIdentity()).add(prop);
}
for(Identity id:toLoad) {
List<Property> props = map.get(id);
if(props == null) {
props = new ArrayList<Property>(1);
}
getOrLoadScorePassedAttemptsMap(id, props, false);
}
count += batch;
} while(count < identities.size());
}
/**
* retrieves the Map which contains all data for this course and the given user.
* if the cache evicted the map in the meantime, then it is recreated
* by querying the database and fetching all that data in one query, and then reput into the cache.
* <br>
* this method is threadsafe.
*
* @param identity the identity
* @param notify if true, then the
* @return a Map containing nodeident+"_"+ e.g. PASSED as key, Boolean (for PASSED), Float (for SCORE), or Integer (for ATTEMPTS) as values
*/
private Map<String, Serializable> getOrLoadScorePassedAttemptsMap(Identity identity, List<Property> properties, boolean prepareForNewData) {
// a user is only active on one node at the same time.
NewCacheKey cacheKey = new NewCacheKey(ores.getResourceableId(), identity.getKey());
HashMap<String, Serializable> m = courseCache.get(cacheKey);
if (m == null) {
// cache entry (=all data of the given identity in this course) has expired or has never been stored yet into the cache.
// or has been invalidated (in cluster mode when puts occurred from an other node for the same cache)
m = new HashMap<String, Serializable>();
// load data
List<Property> loadedProperties = properties == null ? loadPropertiesFor(Collections.singletonList(identity)) : properties;
for (Property property:loadedProperties) {
addPropertyToCache(m, property);
}
//If property not found, prefill with default value.
if(!m.containsKey(ATTEMPTS)) {
m.put(ATTEMPTS, INTEGER_ZERO);
}
if(!m.containsKey(SCORE)) {
m.put(SCORE, FLOAT_ZERO);
}
if(!m.containsKey(LAST_MODIFIED)) {
m.put(LAST_MODIFIED, null);
}
// we use a putSilent here (no invalidation notifications to other cluster nodes), since
// we did not generate new data, but simply asked to reload it.
if (prepareForNewData) {
courseCache.update(cacheKey, m);
} else {
courseCache.put(cacheKey, m);
}
} else {
// still in cache.
if (prepareForNewData) { // but we need to notify that data has changed: we reput the data into the cache - a little hacky yes
courseCache.update(cacheKey, m);
}
}
return m;
}
// package local for perf. reasons, threadsafe.
/**
* puts a property into the cache.
* since it only puts data into a map which in turn is put under the FULLUSERSET key into the cache, we need to
* explicitly reput that key from the cache first, so that the cache notices that that data has changed
* (and can propagate to other nodes if applicable)
*
*/
void putPropertyIntoCache(Identity identity, Property property) {
// load the data, and indicate it to reput into the cache so that the cache knows it is something new.
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, true);
addPropertyToCache(m, property);
}
/**
* Removes a property from cache.
* @param identity
* @param property
*/
void removePropertyFromCache(Identity identity, Property property) {
// load the data, and indicate it to reput into the cache so that the cache knows it is something new.
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, true);
this.removePropertyFromCache(m, property);
}
/**
* thread safe.
* @param property
* @throws AssertionError
*/
private void addPropertyToCache(Map<String, Serializable> acache, Property property) throws AssertionError {
String propertyName = property.getName();
Serializable value;
if (propertyName.equals(ATTEMPTS)) {
value = new Integer(property.getLongValue().intValue());
} else if (propertyName.equals(SCORE)) {
value = property.getFloatValue();
} else if (propertyName.equals(PASSED) || FULLY_ASSESSED.equals(propertyName)) {
value = new Boolean(property.getStringValue());
} else if (propertyName.equals(ASSESSMENT_ID)) {
value = property.getLongValue();
} else if (propertyName.equals(COMMENT) || propertyName.equals(COACH_COMMENT)) {
value = property.getTextValue();
} else {
throw new AssertionError("property in list that is not of type attempts, score, passed or ASSESSMENT_ID, COMMENT and COACH_COMMENT :: " + propertyName);
}
Date lastModified = property.getLastModified();
// put in cache, maybe overriding old values
String cacheKey = getPropertyCacheKey(property);
synchronized(acache) {//cluster_ok acache is an element from the cacher
acache.put(cacheKey, value);
String lmCacheKey = getLastModifiedCacheKey(property);
Long currentLastModifiedDate = (Long)acache.get(lmCacheKey);
if(currentLastModifiedDate == null || currentLastModifiedDate.longValue() < lastModified.getTime()) {
acache.put(lmCacheKey, new Long(lastModified.getTime()));
}
}
}
/**
* Removes property from cache
* @param acache
* @param property
* @throws AssertionError
*/
private void removePropertyFromCache(Map<String, Serializable> acache, Property property) throws AssertionError {
String propertyName = property.getName();
if (!(propertyName.equals(ATTEMPTS) || propertyName.equals(SCORE) || propertyName.equals(PASSED))) {
throw new AssertionError("property in list that is not of type attempts, score or passed ::" + propertyName);
}
String cacheKey = getPropertyCacheKey(property);
synchronized(acache) {//cluster_ok acache is an elment from the cacher
acache.remove(cacheKey);
}
}
/**
*
* @param courseNode
* @param identity
* @param assessedIdentity
* @param score
* @param coursePropManager
*/
void saveNodeScore(CourseNode courseNode, Identity assessedIdentity, Float score, CoursePropertyManager coursePropManager) {
// olat:::: introduce a createOrUpdate method in the cpm and also if applicable in the general propertymanager
if (score != null) {
Property scoreProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, SCORE);
if (scoreProperty == null) {
scoreProperty = coursePropManager.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, SCORE, score, null, null, null);
coursePropManager.saveProperty(scoreProperty);
} else {
scoreProperty.setFloatValue(score);
coursePropManager.updateProperty(scoreProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, scoreProperty);
}
}
/**
*
* @param courseNode
* @param identity
* @param assessedIdentity
* @param mark
* @param coursePropManager
*/
void saveNodeFullyAssessed(CourseNode courseNode, Identity identity, Identity assessedIdentity, Boolean fullyAssessed,
CoursePropertyManager coursePropManager) {
// olat:::: introduce a createOrUpdate method in the cpm and also if applicable in the general propertymanager
if (fullyAssessed != null) {
Property markProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, FULLY_ASSESSED);
if (markProperty == null) {
markProperty = coursePropManager.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, FULLY_ASSESSED, null, null,
String.valueOf(fullyAssessed), null);
coursePropManager.saveProperty(markProperty);
} else {
markProperty.setStringValue(String.valueOf(fullyAssessed));
coursePropManager.updateProperty(markProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, markProperty);
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#saveNodeAttempts(org.olat.course.nodes.CourseNode, org.olat.core.id.Identity, org.olat.core.id.Identity,
* java.lang.Integer)
*/
public void saveNodeAttempts(final CourseNode courseNode, final Identity identity, final Identity assessedIdentity, final Integer attempts) {
// A note on updating the EfficiencyStatement:
// In the equivalent method incrementNodeAttempts() in this class, the following code is executed:
// // Update users efficiency statement
// EfficiencyStatementManager esm = EfficiencyStatementManager.getInstance();
// esm.updateUserEfficiencyStatement(userCourseEnv);
// One would expect that saveNodeAttempts would also have to update the EfficiencyStatement - or
// the caller of this method would have to make sure that this happens in the same transaction.
// While this is not explicitly so, implicitly it is: currently the only user this method is
// the AssessmentEditController - which as the 2nd last method calls into saveScoreEvaluation
// - which in turn does update the EfficiencyStatement - at which point we're happy and everything works fine.
// But it seems like this mechanism is a bit unobvious and might well be worth some refactoring...
ICourse course = CourseFactory.loadCourse(ores);
final CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(createOLATResourceableForLocking(assessedIdentity), new SyncerExecutor(){
public void execute() {
Property attemptsProperty = cpm.findCourseNodeProperty(courseNode, assessedIdentity, null, ATTEMPTS);
if (attemptsProperty == null) {
attemptsProperty = cpm.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, ATTEMPTS,
null, new Long(attempts.intValue()), null, null);
cpm.saveProperty(attemptsProperty);
} else {
attemptsProperty.setLongValue(new Long(attempts.intValue()));
cpm.updateProperty(attemptsProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, attemptsProperty);
}
});
// node log
UserNodeAuditManager am = course.getCourseEnvironment().getAuditManager();
am.appendToUserNodeLog(courseNode, identity, assessedIdentity, ATTEMPTS + " set to: " + String.valueOf(attempts));
// notify about changes
AssessmentChangedEvent ace = new AssessmentChangedEvent(AssessmentChangedEvent.TYPE_ATTEMPTS_CHANGED, assessedIdentity);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(ace, course);
// user activity logging
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_ATTEMPTS_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiAttempts, "", String.valueOf(attempts)));
}
/**
*
* @param courseNode
* @param identity
* @param assessedIdentity
* @param passed
* @param coursePropManager
*/
void saveNodePassed(CourseNode courseNode, Identity assessedIdentity, Boolean passed, CoursePropertyManager coursePropManager) {
Property passedProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, PASSED);
if (passedProperty == null && passed!=null) {
String pass = passed.toString();
passedProperty = coursePropManager.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, PASSED, null, null, pass, null);
coursePropManager.saveProperty(passedProperty);
} else if (passedProperty!=null){
if (passed!=null) {
passedProperty.setStringValue(passed.toString());
coursePropManager.updateProperty(passedProperty);
} else {
removePropertyFromCache(assessedIdentity,passedProperty);
coursePropManager.deleteProperty(passedProperty);
}
}
//add to cache
if(passed!=null && passedProperty!=null) {
putPropertyIntoCache(assessedIdentity, passedProperty);
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#saveNodeComment(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity, org.olat.core.id.Identity,
* java.lang.String)
*/
public void saveNodeComment(final CourseNode courseNode, final Identity identity, final Identity assessedIdentity, final String comment) {
ICourse course = CourseFactory.loadCourse(ores);
final CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(createOLATResourceableForLocking(assessedIdentity), new SyncerExecutor(){
public void execute() {
Property commentProperty = cpm.findCourseNodeProperty(courseNode, assessedIdentity, null, COMMENT);
if (commentProperty == null) {
commentProperty = cpm.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, COMMENT, null, null, null, comment);
cpm.saveProperty(commentProperty);
} else {
commentProperty.setTextValue(comment);
cpm.updateProperty(commentProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, commentProperty);
}
});
// node log
UserNodeAuditManager am = course.getCourseEnvironment().getAuditManager();
am.appendToUserNodeLog(courseNode, identity, assessedIdentity, COMMENT + " set to: " + comment);
// notify about changes
AssessmentChangedEvent ace = new AssessmentChangedEvent(AssessmentChangedEvent.TYPE_USER_COMMENT_CHANGED, assessedIdentity);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(ace, course);
// user activity logging
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_USERCOMMENT_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiUserComment, "", StringHelper.stripLineBreaks(comment)));
}
/**
* @see org.olat.course.assessment.AssessmentManager#saveNodeCoachComment(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity, java.lang.String)
*/
public void saveNodeCoachComment(final CourseNode courseNode, final Identity assessedIdentity, final String comment) {
ICourse course = CourseFactory.loadCourse(ores);
final CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(createOLATResourceableForLocking(assessedIdentity), new SyncerExecutor(){
public void execute() {
Property commentProperty = cpm.findCourseNodeProperty(courseNode, assessedIdentity, null, COACH_COMMENT);
if (commentProperty == null) {
commentProperty = cpm.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, COACH_COMMENT, null, null, null, comment);
cpm.saveProperty(commentProperty);
} else {
commentProperty.setTextValue(comment);
cpm.updateProperty(commentProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, commentProperty);
}
});
// olat::: no node log here? (because what we did above is a node log with custom text AND by a coach)?
// notify about changes
AssessmentChangedEvent ace = new AssessmentChangedEvent(AssessmentChangedEvent.TYPE_COACH_COMMENT_CHANGED, assessedIdentity);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(ace, course);
// user activity logging
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_COACHCOMMENT_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiCoachComment, "", StringHelper.stripLineBreaks(comment)));
}
/**
* @see org.olat.course.assessment.AssessmentManager#incrementNodeAttempts(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public void incrementNodeAttempts(CourseNode courseNode, Identity identity, UserCourseEnvironment userCourseEnv) {
incrementNodeAttempts(courseNode, identity, userCourseEnv, true);
}
/**
* @see org.olat.course.assessment.AssessmentManager#incrementNodeAttemptsInBackground(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity, org.olat.course.run.userview.UserCourseEnvironment)
*/
public void incrementNodeAttemptsInBackground(CourseNode courseNode, Identity identity, UserCourseEnvironment userCourseEnv) {
incrementNodeAttempts(courseNode, identity, userCourseEnv, false);
}
private void incrementNodeAttempts(final CourseNode courseNode, final Identity identity, final UserCourseEnvironment userCourseEnv, boolean logActivity) {
ICourse course = CourseFactory.loadCourse(ores);
final CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
long attempts = CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(createOLATResourceableForLocking(identity), new SyncerCallback<Long>(){
public Long execute() {
long attempts = incrementNodeAttemptsProperty(courseNode, identity, cpm);
if(courseNode instanceof AssessableCourseNode) {
// Update users efficiency statement
EfficiencyStatementManager esm = CoreSpringFactory.getImpl(EfficiencyStatementManager.class);
esm.updateUserEfficiencyStatement(userCourseEnv);
}
return attempts;
}
});
// notify about changes
AssessmentChangedEvent ace = new AssessmentChangedEvent(AssessmentChangedEvent.TYPE_ATTEMPTS_CHANGED, identity);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(ace, course);
if(logActivity) {
// user activity logging
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_ATTEMPTS_UPDATED,
getClass(),
LoggingResourceable.wrap(identity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiAttempts, "", String.valueOf(attempts)));
}
}
/**
* Private method. Increments the attempts property.
* @param courseNode
* @param identity
* @param cpm
* @return the resulting new number of node attempts
*/
private long incrementNodeAttemptsProperty(CourseNode courseNode, Identity identity, CoursePropertyManager cpm) {
Long attempts;
Property attemptsProperty = cpm.findCourseNodeProperty(courseNode, identity, null, ATTEMPTS);
if (attemptsProperty == null) {
attempts = new Long(1);
attemptsProperty = cpm.createCourseNodePropertyInstance(courseNode, identity, null, ATTEMPTS, null, attempts, null, null);
cpm.saveProperty(attemptsProperty);
} else {
attempts = new Long(attemptsProperty.getLongValue().longValue() + 1);
attemptsProperty.setLongValue(attempts);
cpm.updateProperty(attemptsProperty);
}
// add to cache
putPropertyIntoCache(identity, attemptsProperty);
return attempts;
}
/**
* @see org.olat.course.assessment.AssessmentManager#getNodeScore(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public Float getNodeScore(CourseNode courseNode, Identity identity) {
// Check if courseNode exist
if (courseNode == null) {
return FLOAT_ZERO; // return default value
}
String cacheKey = getCacheKey(courseNode, SCORE);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
Float result = (Float) m.get(cacheKey);
return result;
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#getNodePassed(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public Boolean getNodePassed(CourseNode courseNode, Identity identity) {
// Check if courseNode exist
if (courseNode == null) {
return Boolean.FALSE; // return default value
}
String cacheKey = getCacheKey(courseNode, PASSED);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
Boolean result = (Boolean) m.get(cacheKey);
return result;
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#getNodeAttempts(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public Integer getNodeAttempts(CourseNode courseNode, Identity identity) {
// Check if courseNode exist
if (courseNode == null) {
return INTEGER_ZERO; // return default value
}
String cacheKey = getCacheKey(courseNode, ATTEMPTS);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
Integer result = (Integer) m.get(cacheKey);
// see javadoc of org.olat.course.assessment.AssessmentManager#getNodeAttempts
return result == null? INTEGER_ZERO : result;
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#getNodeComment(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public String getNodeComment(CourseNode courseNode, Identity identity) {
if (courseNode == null) {
return null; // return default value
}
String cacheKey = getCacheKey(courseNode, COMMENT);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
String result = (String) m.get(cacheKey);
return result;
}
}
/**
* @see org.olat.course.assessment.AssessmentManager#getNodeCoachComment(org.olat.course.nodes.CourseNode,
* org.olat.core.id.Identity)
*/
public String getNodeCoachComment(CourseNode courseNode, Identity identity) {
if (courseNode == null) {
return null; // return default value
}
String cacheKey = getCacheKey(courseNode, COACH_COMMENT);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
String result = (String) m.get(cacheKey);
return result;
}
}
public Boolean getNodeFullyAssessed(CourseNode courseNode, Identity identity) {
Boolean fullyAssessed = null;
if (courseNode != null) {
String cacheKey = getCacheKey(courseNode, FULLY_ASSESSED);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized (m) {//o_clusterOK by:fj is per vm only
fullyAssessed = (Boolean) m.get(cacheKey);
}
}
return fullyAssessed;
}
/**
* Internal method to create a cache key for a given node, and property
* @param identity
* @param nodeIdent
* @param propertyName
* @return String the key
*/
private String getCacheKey(CourseNode courseNode, String propertyName) {
String nodeIdent = courseNode.getIdent();
return getCacheKey(nodeIdent, propertyName);
}
/**
* threadsafe.
* @param nodeIdent
* @param propertyName
* @return
*/
private String getCacheKey(String nodeIdent, String propertyName) {
StringBuilder key = new StringBuilder(nodeIdent.length()+propertyName.length()+1);
key.append(nodeIdent).append('_').append(propertyName);
return key.toString();
}
/**
* Finds the cacheKey for the input property.
* @param property
* @return Returns the cacheKey
*/
private String getPropertyCacheKey(Property property) {
//- node id is coded into property category like this: NID:ms::12345667
// olat::: move the extract method below to the CoursePropertyManager - since the generation/concat method is also there.
String propertyName = property.getName();
String propertyCategory = property.getCategory();
String nodeIdent = propertyCategory.substring(propertyCategory.indexOf("::") + 2);
String cacheKey = getCacheKey(nodeIdent, propertyName);
//cacheKey is now e.g. 12345667_PASSED
return cacheKey;
}
private String getLastModifiedCacheKey(Property property) {;
String propertyCategory = property.getCategory();
String nodeIdent = propertyCategory.substring(propertyCategory.indexOf("::") + 2);
String cacheKey = getCacheKey(nodeIdent, LAST_MODIFIED);
return cacheKey;
}
/**
* @see org.olat.course.assessment.AssessmentManager#registerForAssessmentChangeEvents(org.olat.core.util.event.GenericEventListener,
* org.olat.core.id.Identity)
*/
public void registerForAssessmentChangeEvents(GenericEventListener gel, Identity identity) {
CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(gel, identity, ores);
}
/**
* @see org.olat.course.assessment.AssessmentManager#deregisterFromAssessmentChangeEvents(org.olat.core.util.event.GenericEventListener)
*/
public void deregisterFromAssessmentChangeEvents(GenericEventListener gel) {
CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(gel, ores);
}
/**
*
* @param courseNode
* @param assessedIdentity
* @param assessmentID
* @param coursePropManager
*/
void saveAssessmentID(CourseNode courseNode, Identity assessedIdentity, Long assessmentID, CoursePropertyManager coursePropManager) {
if(assessmentID!=null) {
Property assessmentIDProperty = coursePropManager.findCourseNodeProperty(courseNode, assessedIdentity, null, ASSESSMENT_ID);
if (assessmentIDProperty == null) {
assessmentIDProperty = coursePropManager.createCourseNodePropertyInstance(courseNode, assessedIdentity, null, ASSESSMENT_ID, null, assessmentID, null, null);
coursePropManager.saveProperty(assessmentIDProperty);
} else {
assessmentIDProperty.setLongValue(assessmentID);
coursePropManager.updateProperty(assessmentIDProperty);
}
// add to cache
putPropertyIntoCache(assessedIdentity, assessmentIDProperty);
}
}
/**
* No caching for the assessmentID.
* @see org.olat.course.assessment.AssessmentManager#getAssessmentID(org.olat.course.nodes.CourseNode, org.olat.core.id.Identity)
*/
public Long getAssessmentID(CourseNode courseNode, Identity identity) {
if (courseNode == null) {
return Long.valueOf(0); // return default value
}
String cacheKey = getCacheKey(courseNode, ASSESSMENT_ID);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
Long result = (Long) m.get(cacheKey);
return result;
}
}
public Date getScoreLastModifiedDate(CourseNode courseNode, Identity identity) {
if (courseNode == null) {
return null; // return default value
}
String cacheKey = getCacheKey(courseNode, LAST_MODIFIED);
Map<String, Serializable> m = getOrLoadScorePassedAttemptsMap(identity, null, false);
synchronized(m) {//o_clusterOK by:fj is per vm only
Long lastModified = (Long) m.get(cacheKey);
if(lastModified != null) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(lastModified.longValue());
return cal.getTime();
}
}
return null;
}
/**
*
* @see org.olat.course.assessment.AssessmentManager#saveScoreEvaluation(org.olat.course.nodes.CourseNode, org.olat.core.id.Identity, org.olat.core.id.Identity, org.olat.course.run.scoring.ScoreEvaluation)
*/
public void saveScoreEvaluation(final CourseNode courseNode, final Identity identity, final Identity assessedIdentity, final ScoreEvaluation scoreEvaluation,
final UserCourseEnvironment userCourseEnv, final boolean incrementUserAttempts) {
final ICourse course = CourseFactory.loadCourse(ores);
final CoursePropertyManager cpm = course.getCourseEnvironment().getCoursePropertyManager();
final RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry();
// o_clusterREVIEW we could sync on a element finer than course, e.g. the composite course+assessIdentity.
// +: concurrency would be higher
// -: many entries (num of courses * visitors of given course) in the locktable.
// we could also sync on the assessedIdentity.
Long attempts = CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(createOLATResourceableForLocking(assessedIdentity), new SyncerCallback<Long>(){
public Long execute() {
Long attempts = null;
Float score = scoreEvaluation.getScore();
Boolean passed = scoreEvaluation.getPassed();
saveNodeScore(courseNode, assessedIdentity, score, cpm);
saveNodePassed(courseNode, assessedIdentity, passed, cpm);
saveAssessmentID(courseNode, assessedIdentity, scoreEvaluation.getAssessmentID(), cpm);
if(incrementUserAttempts) {
attempts = incrementNodeAttemptsProperty(courseNode, assessedIdentity, cpm);
}
if(courseNode instanceof AssessableCourseNode) {
userCourseEnv.getScoreAccounting().evaluateAll();
// Update users efficiency statement
EfficiencyStatementManager esm = CoreSpringFactory.getImpl(EfficiencyStatementManager.class);
esm.updateUserEfficiencyStatement(userCourseEnv);
}
if(passed != null && passed.booleanValue() && course.getCourseConfig().isAutomaticCertificationEnabled()) {
CertificatesManager certificatesManager = CoreSpringFactory.getImpl(CertificatesManager.class);
if(certificatesManager.isCertificationAllowed(assessedIdentity, courseEntry)) {
CertificateTemplate template = null;
Long templateId = course.getCourseConfig().getCertificateTemplate();
if(templateId != null) {
template = certificatesManager.getTemplateById(templateId);
}
CertificateInfos certificateInfos = new CertificateInfos(assessedIdentity, score, passed);
certificatesManager.generateCertificate(certificateInfos, courseEntry, template, true);
}
}
return attempts;
}});
// node log
UserNodeAuditManager am = course.getCourseEnvironment().getAuditManager();
am.appendToUserNodeLog(courseNode, identity, assessedIdentity, SCORE + " set to: " + String.valueOf(scoreEvaluation.getScore()));
if(scoreEvaluation.getPassed()!=null) {
am.appendToUserNodeLog(courseNode, identity, assessedIdentity, PASSED + " set to: " + scoreEvaluation.getPassed().toString());
} else {
am.appendToUserNodeLog(courseNode, identity, assessedIdentity, PASSED + " set to \"undefined\"");
}
if(scoreEvaluation.getAssessmentID()!=null) {
am.appendToUserNodeLog(courseNode, assessedIdentity, assessedIdentity, ASSESSMENT_ID + " set to: " + scoreEvaluation.getAssessmentID().toString());
}
// notify about changes
AssessmentChangedEvent ace = new AssessmentChangedEvent(AssessmentChangedEvent.TYPE_SCORE_EVAL_CHANGED, assessedIdentity);
CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(ace, course);
// user activity logging
if (scoreEvaluation.getScore()!=null) {
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_SCORE_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiScore, "", String.valueOf(scoreEvaluation.getScore())));
}
if (scoreEvaluation.getPassed()!=null) {
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_PASSED_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiPassed, "", String.valueOf(scoreEvaluation.getPassed())));
} else {
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_PASSED_UPDATED,
getClass(),
LoggingResourceable.wrap(assessedIdentity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiPassed, "", "undefined"));
}
if (incrementUserAttempts && attempts!=null) {
ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_ATTEMPTS_UPDATED,
getClass(),
LoggingResourceable.wrap(identity),
LoggingResourceable.wrapNonOlatResource(StringResourceableType.qtiAttempts, "", String.valueOf(attempts)));
}
}
public Long syncAndsaveScoreEvaluation(CourseNode courseNode, Identity identity, Identity assessedIdentity,
ScoreEvaluation scoreEvaluation, boolean incrementUserAttempts, UserCourseEnvironment userCourseEnv,
CoursePropertyManager cpm) {
return saveScoreEvaluationInSync(courseNode, identity, assessedIdentity, scoreEvaluation, incrementUserAttempts, userCourseEnv, cpm);
}
private Long saveScoreEvaluationInSync(CourseNode courseNode, Identity identity, Identity assessedIdentity, ScoreEvaluation scoreEvaluation,
boolean incrementUserAttempts, final UserCourseEnvironment userCourseEnv, final CoursePropertyManager cpm) {
Long attempts = null;
saveNodeScore(courseNode, assessedIdentity, scoreEvaluation.getScore(), cpm);
log.debug("successfully saved node score : " + scoreEvaluation.getScore());
saveNodePassed(courseNode, assessedIdentity, scoreEvaluation.getPassed(), cpm);
log.debug("successfully saved node passed : " + scoreEvaluation.getPassed());
saveAssessmentID(courseNode, assessedIdentity, scoreEvaluation.getAssessmentID(), cpm);
log.debug("successfully saved node asssessmentId : " + scoreEvaluation.getPassed());
saveNodeFullyAssessed(courseNode, identity, assessedIdentity, scoreEvaluation.getFullyAssessed(), cpm);
log.debug("successfully saved node marked completely : " + scoreEvaluation.getPassed());
if (incrementUserAttempts) {
attempts = incrementNodeAttemptsProperty(courseNode, assessedIdentity, cpm);
log.debug("successfully saved user attemps : " + attempts);
}
saveNodeFullyAssessed(courseNode, identity, assessedIdentity, scoreEvaluation.getFullyAssessed(), cpm);
log.debug("successfully saved node fullyAssessed : " + scoreEvaluation.getFullyAssessed());
DBFactory.getInstance().commitAndCloseSession();
if (courseNode instanceof AssessableCourseNode) {
userCourseEnv.getScoreAccounting().evaluateAll();
EfficiencyStatementManager esm = CoreSpringFactory.getImpl(EfficiencyStatementManager.class);
esm.updateUserEfficiencyStatement(userCourseEnv);
}
return attempts;
}
/**
* Always use this to get a OLATResourceable for doInSync locking!
* Uses the assessIdentity.
*
* @param course
* @param assessedIdentity
* @param courseNode
* @return
*/
public OLATResourceable createOLATResourceableForLocking(Identity assessedIdentity) {
String type = "AssessmentManager::Identity";
OLATResourceable oLATResourceable = OresHelper.createOLATResourceableInstance(type,assessedIdentity.getKey());
return oLATResourceable;
}
}