/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <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 the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <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>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.upgrade;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.BaseSecurityManager;
import org.olat.basesecurity.Constants;
import org.olat.basesecurity.SecurityGroup;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.DBQuery;
import org.olat.core.commons.services.commentAndRating.CommentAndRatingSecurityCallback;
import org.olat.core.commons.services.commentAndRating.manager.UserCommentsDAO;
import org.olat.core.commons.services.commentAndRating.model.UserComment;
import org.olat.core.id.Identity;
import org.olat.portfolio.PortfolioModule;
import org.olat.portfolio.manager.EPFrontendManager;
import org.olat.portfolio.model.artefacts.AbstractArtefact;
import org.olat.portfolio.model.structel.EPPage;
import org.olat.portfolio.model.structel.EPStructureElement;
import org.olat.portfolio.model.structel.EPStructuredMap;
import org.olat.portfolio.model.structel.EPStructuredMapTemplate;
import org.olat.portfolio.model.structel.ElementType;
import org.olat.portfolio.model.structel.PortfolioStructure;
import org.olat.resource.OLATResource;
import org.olat.upgrade.model.BGContextImpl;
import org.olat.upgrade.model.BusinessGroupUpgrade;
import org.olat.upgrade.model.RepositoryEntryUpgrade;
/**
* Description:<br>
* upgrade code for OLAT 7.1.0 -> OLAT 7.1.1
* - fixing invalid structures being built by synchronisation, see OLAT-6316 and OLAT-6306
* - merges all yet found data to last valid node
*
* <P>
* Initial Date: 24.03.2011 <br>
*
* @author Roman Haag, roman.haag@frentix.com, www.frentix.com
*/
public class OLATUpgrade_7_1_1 extends OLATUpgrade {
private static final String TASK_CLEANUP_TEMPLATES = "Cleanup template maps on db directly";
private static final String TASK_CHECK_AND_FIX_TEMPLATEMAPS = "Check templates and fix portfolio-task child elements";
private static final String MIGRATE_SECURITY_GROUP = "Migrate repository entry security groups";
private static final int REPO_ENTRIES_BATCH_SIZE = 20;
private static final String VERSION = "OLAT_7.1.1";
private boolean portfolioCourseNodeEnabled;
private EPFrontendManager ePFMgr;
private UserCommentsDAO commentAndRatingService;
private PortfolioModule epfModule;
public OLATUpgrade_7_1_1(PortfolioModule epfModule) {
super();
this.epfModule = epfModule;
}
public void setPortfolioCourseNodeEnabled(boolean portfolioCourseNodeEnabled){
this.portfolioCourseNodeEnabled = portfolioCourseNodeEnabled;
}
@Override
public boolean doPreSystemInitUpgrade(UpgradeManager upgradeManager) {
return false;
}
@Override
public boolean doPostSystemInitUpgrade(UpgradeManager upgradeManager) {
UpgradeHistoryData uhd = upgradeManager.getUpgradesHistory(VERSION);
if (uhd == null) {
// has never been called, initialize
uhd = new UpgradeHistoryData();
} else {
if (uhd.isInstallationComplete()) return false;
}
if((portfolioCourseNodeEnabled && epfModule.isEnabled())) {
// get template maps with invalid references and fix them
fixInvalidMapReferences(upgradeManager, uhd);
// remove invalid references on db
fixInvalidTemplateMaps(upgradeManager, uhd);
}
migrateSecurityGroups(upgradeManager, uhd);
uhd.setInstallationComplete(true);
upgradeManager.setUpgradesHistory(uhd, VERSION);
log.audit("Finished OLATUpgrade_7_1_1 successfully!");
return true;
}
private void fixInvalidTemplateMaps(UpgradeManager upgradeManager, UpgradeHistoryData uhd){
if (!uhd.getBooleanDataValue(TASK_CLEANUP_TEMPLATES)) {
String query = "UPDATE o_ep_struct_el SET struct_el_source=NULL WHERE struct_el_source not in (Select `structure_id` from (Select * from `o_ep_struct_el`) as t) ;";
executePlainSQLDBStatement(query, upgradeManager.getDataSource());
log.audit("run on DB: removed invalid template references to source-objects");
uhd.setBooleanDataValue(TASK_CLEANUP_TEMPLATES, true);
upgradeManager.setUpgradesHistory(uhd, VERSION);
}
}
private void fixInvalidMapReferences(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
if (!uhd.getBooleanDataValue(TASK_CHECK_AND_FIX_TEMPLATEMAPS)) {
log.audit("+---------------------------------------------------------------------------------------+");
log.audit("+... check templates and collect lost sub-structures to actualy bound pages/structs ...+");
log.audit("+---------------------------------------------------------------------------------------+");
ePFMgr = (EPFrontendManager) CoreSpringFactory.getBean("epFrontendManager");
int amount = 10;
int count = countPortfolioTemplates();
log.audit("found a total of " + count + " portfolio templates. check stepwise with stepsize: " + amount);
for (int start = 0; start < count; start = start + amount) {
List<PortfolioStructure> templates = ePFMgr.getStructureElements(start, amount, ElementType.TEMPLATE_MAP);
log.audit("start at " + start + " . Will check (next) " + templates.size() + " portfolio task for irregularities.");
templates = getInvalidTemplates(templates);
log.audit("#1: found " + templates.size() + " templates that look invalid and might have produced bad portfolio-tasks!");
for (PortfolioStructure templateStruct : templates) {
log.audit(" #2: handling Structured Maps (maps collected from a portfolio task) for template " + templateStruct.getKey() + " "
+ templateStruct.getTitle());
// get corresponding portfolio-tasks taken from templates
List<PortfolioStructure> structMaps = getStructuredMapsLinkedToTemplate(templateStruct);
log.audit(" this task has been taken " + structMaps.size() + " times.");
for (PortfolioStructure structuredMap : structMaps) {
log.audit(" #3: now work on StructuredMap: " + structuredMap);
processStructuredMapDeeply(structuredMap);
} // loop struct-maps
// fix template with children
fixTemplateEntry(templateStruct);
DBFactory.getInstance().intermediateCommit();
} // loop templates
} // steps
DBFactory.getInstance().intermediateCommit();
}
uhd.setBooleanDataValue(TASK_CHECK_AND_FIX_TEMPLATEMAPS, true);
upgradeManager.setUpgradesHistory(uhd, VERSION);
}
private void processStructuredMapDeeply(PortfolioStructure structuredMap){
// check children (StructureElements) first, because of later recursive removal!
List<PortfolioStructure> linkedChildren = ePFMgr.loadStructureChildren(structuredMap);
for (PortfolioStructure linkedPage : linkedChildren) {
log.audit(" #4: processing structs of page: " + linkedPage);
processStructure(linkedPage);
}
log.audit(" #5: processing pages of map: " + structuredMap);
processStructure(structuredMap);
}
/**
* call the needed methods to really do the magic.
*/
private void processStructure(PortfolioStructure structuredMap) {
List<PortfolioStructure> linkedChildren = ePFMgr.loadStructureChildren(structuredMap);
for (PortfolioStructure childStruct : linkedChildren) {
// reload dbchildren in each run, because of filtering
List<PortfolioStructure> dbChildren;
if (childStruct instanceof EPPage) {
dbChildren = loadMapChildrenByInternalFK(childStruct, structuredMap);
} else {
dbChildren = loadMapChildrenByInternalFK(childStruct, structuredMap.getRoot());
}
// filter the struct that is already linked by a struct->struct link
// and filter such with other struct-source
filterLinkedStructs(childStruct, dbChildren);
log.audit(" found " + dbChildren.size() + " faulty children (of " + childStruct + ") linked by internal fk to be merged and removed afterwards.");
// merge artefacts to the linked struct
mergeLinkedArtefactsToFinalStruct(childStruct, dbChildren);
// collect comments on structures and merge to target
mergeCommentsToFinalStruct(childStruct, dbChildren);
// ratings are not merged, see OLAT-6306
// remove the old elements
removeWrongElements(dbChildren);
// fix root-reference on EPStructureElements
if (!(childStruct instanceof EPPage)) {
if (childStruct.getRoot().equals(childStruct.getRootMap())) {
EPStructureElement realParentRoot = (EPStructureElement) ePFMgr.loadStructureParent(childStruct);
((EPStructureElement)childStruct).setRoot(realParentRoot);
ePFMgr.savePortfolioStructure(childStruct);
}
}
} // loop children
DBFactory.getInstance().intermediateCommit();
}
private void fixTemplateEntry(PortfolioStructure template){
List<PortfolioStructure> temps = new ArrayList<PortfolioStructure>();
recurseIntoTemplateAndCheck(new ArrayList<PortfolioStructure>(), temps, template);
for (PortfolioStructure portfolioStructure : temps) {
((EPStructureElement)portfolioStructure).setStructureElSource(null);
ePFMgr.savePortfolioStructure(portfolioStructure);
}
}
private void removeWrongElements(List<PortfolioStructure> wrongStructs){
for (PortfolioStructure portfolioStructure : wrongStructs) {
ePFMgr.deletePortfolioStructure(portfolioStructure);
}
}
private List<PortfolioStructure> getInvalidTemplates(List<PortfolioStructure> templates) {
List<PortfolioStructure> temps = new ArrayList<PortfolioStructure>();
for (PortfolioStructure portfolioStructure : templates) {
recurseIntoTemplateAndCheck(temps, new ArrayList<PortfolioStructure>(),portfolioStructure);
}
// get unique root-templates
HashSet<PortfolioStructure> h = new HashSet<PortfolioStructure>(temps);
temps.clear();
temps.addAll(h);
return temps;
}
private void recurseIntoTemplateAndCheck(List<PortfolioStructure> resultingMaps, List<PortfolioStructure> resultingStructs, PortfolioStructure struct){
List<PortfolioStructure> children = ePFMgr.loadStructureChildren(struct);
if (children == null || children.isEmpty()) return;
for (PortfolioStructure portfolioStructure : children) {
if (((EPStructureElement)portfolioStructure).getStructureElSource() != null) {
resultingMaps.add(portfolioStructure.getRootMap());
resultingStructs.add(portfolioStructure);
}
recurseIntoTemplateAndCheck(resultingMaps, resultingStructs, portfolioStructure);
}
return;
}
private void mergeCommentsToFinalStruct(PortfolioStructure finalStruct, List<PortfolioStructure> wrongStructs){
if (wrongStructs == null || wrongStructs.isEmpty()) return;
List<UserComment> collectedComments = new ArrayList<UserComment>();
// collect all comments out there
for (PortfolioStructure portfolioStructure : wrongStructs) {
if (!(portfolioStructure instanceof EPPage)) return; // no comments on StructureElements!
List<UserComment> oldComments = commentAndRatingService.getComments(portfolioStructure.getRootMap(), portfolioStructure.getKey().toString());
collectedComments.addAll(oldComments);
commentAndRatingService.deleteAllComments(portfolioStructure.getRootMap(), portfolioStructure.getKey().toString());
}
log.audit(" found " + collectedComments.size() + " comments for this structure, will be merged to new destination.");
if (collectedComments.size() == 0) return;
Identity ident = collectedComments.get(0).getCreator();
UserComment topComment = commentAndRatingService.createComment(ident, finalStruct.getRootMap(), finalStruct.getKey().toString(), "The following comments were restored from a migration task to rescue lost data.");
// attach all to this info-comment
for (UserComment userComment : collectedComments) {
UserComment attachedComment = commentAndRatingService.replyTo(topComment, userComment.getCreator(), userComment.getComment());
// set original date
attachedComment.setCreationDate(userComment.getCreationDate());
commentAndRatingService.updateComment(attachedComment, attachedComment.getComment());
}
}
private void mergeLinkedArtefactsToFinalStruct(PortfolioStructure finalStruct, List<PortfolioStructure> wrongStructs){
if (wrongStructs == null || wrongStructs.isEmpty()) return;
// temporarily remove the collectrestriction, will be added by next sync.
// TODO: somehow user must be warned, that map may contain too much artefacts on one node
finalStruct.getCollectRestrictions().clear();
ePFMgr.savePortfolioStructure(finalStruct);
int artefactCount = 0;
for (PortfolioStructure portfolioStructure : wrongStructs) {
portfolioStructure = ePFMgr.loadPortfolioStructureByKey(portfolioStructure.getKey());
List<AbstractArtefact> artefacts = ePFMgr.getArtefacts(portfolioStructure);
for (AbstractArtefact abstractArtefact : artefacts) {
if (!ePFMgr.isArtefactInStructure(abstractArtefact, finalStruct)){
artefactCount++;
ePFMgr.moveArtefactFromStructToStruct(abstractArtefact, portfolioStructure, finalStruct);
} else {
log.audit("An Artefact " + abstractArtefact + " has already been added to new target, therefore will be removed from wrong structure.");
// TODO: maybe we should save the reflexion on the link artefact -> structure!
ePFMgr.removeArtefactFromStructure(abstractArtefact, portfolioStructure);
}
}
}
log.audit(" merged " + artefactCount + " artefacts to new destinations.");
}
private void filterLinkedStructs(PortfolioStructure filterBase, List<PortfolioStructure> dbChildren){
for (Iterator<PortfolioStructure> iterator = dbChildren.iterator(); iterator.hasNext();) {
PortfolioStructure portfolioStructure = iterator.next();
long filterBaseSourceKey = ((EPStructureElement)filterBase).getStructureElSource();
long structSourceKey = ((EPStructureElement)portfolioStructure).getStructureElSource();
if (portfolioStructure.getKey() == filterBase.getKey() || filterBaseSourceKey != structSourceKey){
iterator.remove();
}
}
}
// helper to get child not by link, but internal direct link to root-element
private List<PortfolioStructure> loadMapChildrenByInternalFK(PortfolioStructure linkedChildStruct, PortfolioStructure root){
if (root == null) return null;
StringBuilder sb = new StringBuilder();
sb.append("select stEl from ").append(EPStructureElement.class.getName()).append(" stEl");
// sb.append(" where stEl.root=:rootEl and stEl.structureElSource=:sourceEl"); // filtered by filterLinkedStructs()
sb.append(" where stEl.root=:rootEl");
DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
query.setEntity("rootEl", root);
// query.setLong("sourceEl", ((EPStructureElement)linkedChildStruct).getStructureElSource());
@SuppressWarnings("unchecked")
List<PortfolioStructure> resources = query.list();
return resources;
}
private int countPortfolioTemplates(){
StringBuilder sb = new StringBuilder();
sb.append("select count(stEl) from ").append(EPStructureElement.class.getName()).append(" stEl");
sb.append(" where stEl.class in (" + EPStructuredMapTemplate.class.getName() + ")");
DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
Number count = (Number)query.uniqueResult();
return count.intValue();
}
private List<PortfolioStructure> getStructuredMapsLinkedToTemplate(PortfolioStructure template){
StringBuilder sb = new StringBuilder();
sb.append("select map from ").append(EPStructuredMap.class.getName()).append(" map")
.append(" where map.structuredMapSource=:template");
DBQuery query = DBFactory.getInstance().createQuery(sb.toString());
query.setEntity("template", template);
@SuppressWarnings("unchecked")
List<PortfolioStructure> maps = query.list();
return maps;
}
//fxdiff VCRP-1: access control repository entry
private void migrateSecurityGroups(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
if (!uhd.getBooleanDataValue(MIGRATE_SECURITY_GROUP)) {
log.audit("+-----------------------------------------------------------------------------+");
log.audit("+... Migrate the repository entry security groups from business groups ...+");
log.audit("+-----------------------------------------------------------------------------+");
int counter = 0;
List<RepositoryEntryUpgrade> entries;
do {
entries = queryEntries(counter);
for(RepositoryEntryUpgrade entry:entries) {
createRepoEntrySecurityGroups(entry);
migrateRepoEntrySecurityGroups(entry);
}
counter += entries.size();
log.audit("Processed entries: " + entries.size());
} while(entries.size() == REPO_ENTRIES_BATCH_SIZE);
log.audit("+... Migration processed " + counter + " repository entries ...+");
uhd.setBooleanDataValue(MIGRATE_SECURITY_GROUP, true);
upgradeManager.setUpgradesHistory(uhd, VERSION);
}
}
private void createRepoEntrySecurityGroups(RepositoryEntryUpgrade entry) {
BaseSecurity securityManager = BaseSecurityManager.getInstance();
boolean save = false;
if(entry.getTutorGroup() == null) {
// security group for tutors / coaches
SecurityGroup tutorGroup = securityManager.createAndPersistSecurityGroup();
// member of this group may modify member's membership
securityManager.createAndPersistPolicy(tutorGroup, Constants.PERMISSION_ACCESS, entry.getOlatResource());
// members of this group are always tutors also
securityManager.createAndPersistPolicy(tutorGroup, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_TUTOR);
entry.setTutorGroup(tutorGroup);
securityManager.createAndPersistPolicy(entry.getTutorGroup(), Constants.PERMISSION_COACH, entry.getOlatResource());
DBFactory.getInstance().commit();
save = true;
}
if(entry.getParticipantGroup() == null) {
// security group for participants
SecurityGroup participantGroup = securityManager.createAndPersistSecurityGroup();
// member of this group may modify member's membership
securityManager.createAndPersistPolicy(participantGroup, Constants.PERMISSION_ACCESS, entry.getOlatResource());
// members of this group are always participants also
securityManager.createAndPersistPolicy(participantGroup, Constants.PERMISSION_HASROLE, Constants.ORESOURCE_PARTICIPANT);
entry.setParticipantGroup(participantGroup);
securityManager.createAndPersistPolicy(entry.getParticipantGroup(), Constants.PERMISSION_PARTI, entry.getOlatResource());
DBFactory.getInstance().commit();
save = true;
}
if(save) {
DBFactory.getInstance().updateObject(entry);
}
}
private void migrateRepoEntrySecurityGroups(RepositoryEntryUpgrade entry) {
BaseSecurity securityManager = BaseSecurityManager.getInstance();
List<BGContextImpl> contexts = findBGContextsForResource(entry.getOlatResource(), true, true);
for(BGContextImpl context:contexts) {
List<BusinessGroupUpgrade> groups = getGroupsOfBGContext(context);
for(BusinessGroupUpgrade group:groups) {
//migrate tutors
if(group.getOwnerGroup() != null) {
int count = 0;
List<Identity> owners = securityManager.getIdentitiesOfSecurityGroup(group.getOwnerGroup());
SecurityGroup tutorGroup = entry.getTutorGroup();
for(Identity owner:owners) {
if(securityManager.isIdentityInSecurityGroup(owner, tutorGroup)) {
continue;
}
securityManager.addIdentityToSecurityGroup(owner, tutorGroup);
if(count++ % 20 == 0) {
DBFactory.getInstance().intermediateCommit();
}
}
DBFactory.getInstance().intermediateCommit();
}
//migrate participants
if(group.getPartipiciantGroup() != null) {
int count = 0;
List<Identity> participants = securityManager.getIdentitiesOfSecurityGroup(group.getPartipiciantGroup());
SecurityGroup participantGroup = entry.getParticipantGroup();
for(Identity participant:participants) {
if(securityManager.isIdentityInSecurityGroup(participant, participantGroup)) {
continue;
}
securityManager.addIdentityToSecurityGroup(participant, participantGroup);
if(count++ % 20 == 0) {
DBFactory.getInstance().intermediateCommit();
}
}
DBFactory.getInstance().intermediateCommit();
}
}
}
}
private List<BusinessGroupUpgrade> getGroupsOfBGContext(BGContextImpl bgContext) {
String q = "select bg from org.olat.upgrade.model.BusinessGroupImpl bg where bg.groupContextKey = :contextKey";
DBQuery query = DBFactory.getInstance().createQuery(q);
query.setLong("contextKey", bgContext.getKey());
@SuppressWarnings("unchecked")
List<BusinessGroupUpgrade> groups = query.list();
return groups;
}
public List<RepositoryEntryUpgrade> queryEntries(int firstResult) {
StringBuilder sb = new StringBuilder();
sb.append("select v from ").append(RepositoryEntryUpgrade.class.getName()).append(" v inner join fetch v.olatResource as res order by v.key asc");
DBQuery dbquery = DBFactory.getInstance().createQuery(sb.toString());
dbquery.setFirstResult(firstResult);
dbquery.setMaxResults(REPO_ENTRIES_BATCH_SIZE);
@SuppressWarnings("unchecked")
List<RepositoryEntryUpgrade> entries = dbquery.list();
return entries;
}
private List<BGContextImpl> findBGContextsForResource(OLATResource resource, boolean defaultContexts, boolean nonDefaultContexts) {
DB db = DBFactory.getInstance();
StringBuilder q = new StringBuilder();
q.append(" select context from org.olat.group.context.BGContextImpl as context,");
q.append(" org.olat.group.context.BGContext2Resource as bgcr");
q.append(" where bgcr.resource = :resource");
q.append(" and bgcr.groupContext = context");
boolean checkDefault = defaultContexts != nonDefaultContexts;
if (checkDefault){
q.append(" and context.defaultContext = :isDefault");
}
DBQuery query = db.createQuery(q.toString());
query.setEntity("resource", resource);
if (checkDefault){
query.setBoolean("isDefault", defaultContexts ? true : false);
}
@SuppressWarnings("unchecked")
List<BGContextImpl> contexts = query.list();
return contexts;
}
@Override
public String getVersion() {
return VERSION;
}
/**
* allow everything to do with comments/rating
*/
public class FullCommentAndRatingSecCallback implements CommentAndRatingSecurityCallback {
public boolean canViewComments() {
return true;
}
public boolean canCreateComments() {
return true;
}
public boolean canReplyToComment(UserComment comment) {
return true;
}
public boolean canUpdateComment(UserComment comment, List<UserComment> allComments) {
return true;
}
public boolean canDeleteComment(UserComment comment) {
return true;
}
public boolean canViewRatingAverage() {
return true;
}
public boolean canViewOtherUsersRatings() {
return true;
}
public boolean canRate() {
return true;
}
}
}