/** * <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.course.nodes.cl.manager; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import org.olat.basesecurity.Group; import org.olat.basesecurity.IdentityImpl; import org.olat.core.commons.modules.bc.FolderConfig; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.util.StringHelper; import org.olat.core.util.vfs.VFSContainer; import org.olat.course.nodes.CheckListCourseNode; import org.olat.course.nodes.cl.CheckboxManager; import org.olat.course.nodes.cl.model.AssessmentBatch; import org.olat.course.nodes.cl.model.AssessmentData; import org.olat.course.nodes.cl.model.Checkbox; import org.olat.course.nodes.cl.model.CheckboxList; import org.olat.course.nodes.cl.model.DBCheck; import org.olat.course.nodes.cl.model.DBCheckbox; import org.olat.course.run.environment.CourseEnvironment; import org.olat.group.BusinessGroup; import org.olat.modules.vitero.model.GroupRole; import org.olat.repository.RepositoryEntry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * * Initial date: 06.02.2014<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ @Service public class CheckboxManagerImpl implements CheckboxManager { @Autowired private DB dbInstance; @Override public DBCheckbox createDBCheckbox(String checkboxId, OLATResourceable ores, String resSubPath) { DBCheckbox checkbox = new DBCheckbox(); checkbox.setCreationDate(new Date()); checkbox.setLastModified(new Date()); checkbox.setCheckboxId(checkboxId); checkbox.setResName(ores.getResourceableTypeName()); checkbox.setResId(ores.getResourceableId()); checkbox.setResSubPath(resSubPath); dbInstance.getCurrentEntityManager().persist(checkbox); return checkbox; } @Override public List<DBCheckbox> loadCheckbox(OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select box from clcheckbox box where box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<DBCheckbox> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheckbox.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } return query.getResultList(); } @Override public DBCheckbox loadCheckbox(OLATResourceable ores, String resSubPath, String checkboxId) { StringBuilder sb = new StringBuilder(); sb.append("select box from clcheckbox box") .append(" where box.checkboxId=:checkboxId and box.resName=:resName and box.resId=:resId") .append(" and box.resSubPath=:resSubPath"); List<DBCheckbox> box = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheckbox.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("resSubPath", resSubPath) .setParameter("checkboxId", checkboxId) .getResultList(); if(box.isEmpty()) { return null; } return box.get(0); } private DBCheckbox loadForUpdate(DBCheckbox checkbox) { dbInstance.getCurrentEntityManager().detach(checkbox); StringBuilder sb = new StringBuilder(); sb.append("select box from clcheckbox box where box.key=:key"); List<DBCheckbox> box = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheckbox.class) .setParameter("key", checkbox.getKey()) .getResultList(); if(box.isEmpty()) { return null; } return box.get(0); } private List<DBCheckbox> loadCheckbox(OLATResourceable ores, String resSubPath, Collection<String> uuids) { if(uuids == null || uuids.isEmpty()) return Collections.emptyList(); StringBuilder sb = new StringBuilder(); sb.append("select box from clcheckbox box") .append(" where box.checkboxId in (:checkboxId) and box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<DBCheckbox> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheckbox.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("checkboxId", uuids); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } return query.getResultList(); } @Override public void removeCheckbox(DBCheckbox checkbox) { DBCheckbox ref = dbInstance.getCurrentEntityManager() .getReference(DBCheckbox.class, checkbox.getKey()); dbInstance.getCurrentEntityManager().remove(ref); } @Override public List<DBCheck> loadCheck(OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select check from clcheck check") .append(" inner join fetch check.checkbox box") .append(" where box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<DBCheck> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheck.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } return query.getResultList(); } @Override public List<DBCheck> loadCheck(Identity identity, OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select check from clcheck check") .append(" inner join fetch check.checkbox box") .append(" where check.identity.key=:identityKey") .append(" and box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<DBCheck> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheck.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("identityKey", identity.getKey()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } return query.getResultList(); } @Override public void syncCheckbox(CheckboxList checkboxList, OLATResourceable ores, String resSubPath) { List<DBCheckbox> dbCheckboxList = loadCheckbox(ores, resSubPath); Map<String,DBCheckbox> uuids = new HashMap<String,DBCheckbox>(); for(DBCheckbox dbCheckbox:dbCheckboxList) { uuids.put(dbCheckbox.getCheckboxId(), dbCheckbox); } if(checkboxList != null && checkboxList.getList() != null) { List<Checkbox> resCheckboxList = checkboxList.getList(); for(Checkbox resCheckbox:resCheckboxList) { String resUuid = resCheckbox.getCheckboxId(); if(uuids.containsKey(resUuid)) { uuids.remove(resUuid);//already synched } else { createDBCheckbox(resUuid, ores, resSubPath); } } } for(DBCheckbox dbCheckbox:uuids.values()) { System.out.println("Remove them??? " + dbCheckbox.getCheckboxId()); } } @Override public void deleteCheckbox(OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("delete from clcheck check") .append(" where check.checkbox.key in (") .append(" select box.key from clcheckbox box where box.resName=:resName and box.resId=:resId and box.resSubPath=:resSubPath") .append(" )"); dbInstance.getCurrentEntityManager() .createQuery(sb.toString()) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("resSubPath", resSubPath) .executeUpdate(); StringBuilder sb2 = new StringBuilder(); sb2.append("delete from clcheckbox box") .append(" where box.resName=:resName and box.resId=:resId and box.resSubPath=:resSubPath"); dbInstance.getCurrentEntityManager() .createQuery(sb2.toString()) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("resSubPath", resSubPath) .executeUpdate(); } @Override public void check(DBCheckbox checkbox, Identity owner, Float score, Boolean checked) { DBCheck currentCheck = loadCheck(checkbox, owner); if(currentCheck == null) { DBCheckbox lockedCheckbox = loadForUpdate(checkbox); if(lockedCheckbox != null) { //locked -> reload to make sure nobody create it DBCheck reloadedCheck = loadCheck(checkbox, owner); if(reloadedCheck == null) { createCheck(lockedCheckbox, owner, score, checked); } else { currentCheck = reloadedCheck; } } } if(currentCheck != null) { currentCheck.setScore(score); currentCheck.setChecked(checked); } dbInstance.commit(); } @Override public void check(OLATResourceable ores, String resSubPath, List<AssessmentBatch> batch) { Collections.sort(batch, new BatchComparator()); EntityManager em = dbInstance.getCurrentEntityManager(); Set<String> dbBoxUuids = new HashSet<>(); for(AssessmentBatch row:batch) { dbBoxUuids.add(row.getCheckboxId()); } List<DBCheckbox> boxes = loadCheckbox(ores, resSubPath, dbBoxUuids); Map<String, DBCheckbox> uuidToBox = new HashMap<>(); for(DBCheckbox box:boxes) { uuidToBox.put(box.getCheckboxId(), box); } Identity currentIdentity = null; for(AssessmentBatch row:batch) { Long identityKey = row.getIdentityKey(); if(currentIdentity == null || !identityKey.equals(currentIdentity.getKey())) { currentIdentity = em.getReference(IdentityImpl.class, identityKey); } boolean check = row.getCheck(); DBCheckbox checkbox = uuidToBox.get(row.getCheckboxId()); DBCheck currentCheck = loadCheck(checkbox, currentIdentity); if(check) { if(currentCheck == null) { DBCheckbox lockedCheckbox = loadForUpdate(checkbox); if(lockedCheckbox != null) { //locked -> reload to make sure nobody create it DBCheck reloaedCheck = loadCheck(checkbox, currentIdentity); if(reloaedCheck == null) { createCheck(lockedCheckbox, currentIdentity, row.getScore(), new Boolean(check)); } else { currentCheck = reloaedCheck; } } dbInstance.commit(); } if(currentCheck != null) { currentCheck.setScore(row.getScore()); currentCheck.setChecked(new Boolean(check)); em.merge(currentCheck); } //save check } else if(currentCheck != null) { currentCheck.setChecked(Boolean.FALSE); currentCheck.setScore(new Float(0f)); em.merge(currentCheck); } } } protected DBCheck loadCheck(DBCheckbox checkbox, Identity identity) { if(checkbox == null || identity == null) return null; StringBuilder sb = new StringBuilder(); sb.append("select check from clcheck as check") .append(" where check.identity.key=:identityKey and check.checkbox.key=:checkboxKey"); List<DBCheck> checks = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheck.class) .setParameter("identityKey", identity.getKey()) .setParameter("checkboxKey", checkbox.getKey()) .getResultList(); if(checks.isEmpty()) { return null; } return checks.get(0); } protected DBCheck createCheck(DBCheckbox checkbox, Identity owner, Float score, Boolean checked) { DBCheck check = new DBCheck(); check.setCreationDate(new Date()); check.setLastModified(new Date()); check.setIdentity(owner); check.setCheckbox(checkbox); check.setChecked(checked); check.setScore(score); dbInstance.getCurrentEntityManager().persist(check); return check; } @Override public int countChecks(OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select count(check) from clcheck check") .append(" inner join check.checkbox box") .append(" inner join check.identity ident") .append(" where box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<Number> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Number.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } Number numOfChecks = query.getSingleResult(); return numOfChecks.intValue(); } @Override public int countChecked(Identity identity, OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select count(check) from clcheck check") .append(" inner join check.checkbox box") .append(" where check.identity.key=:identityKey and box.resName=:resName and box.resId=:resId") .append(" and check.checked=true"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<Number> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Number.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("identityKey", identity.getKey()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } Number numOfChecks = query.getSingleResult(); return numOfChecks.intValue(); } @Override public float calculateScore(Identity identity, OLATResourceable ores, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select sum(check.score) from clcheck check") .append(" inner join check.checkbox box") .append(" where check.identity.key=:identityKey and check.checked=true and box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } TypedQuery<Number> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), Number.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()) .setParameter("identityKey", identity.getKey()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } Number numOfChecks = query.getSingleResult(); return numOfChecks == null ? 0.0f : numOfChecks.floatValue(); } @Override public List<AssessmentData> getAssessmentDatas(OLATResourceable ores, String resSubPath, RepositoryEntry re, List<BusinessGroup> businessGroups) { StringBuilder sb = new StringBuilder(); sb.append("select check from clcheck check") .append(" inner join fetch check.checkbox box") .append(" inner join fetch check.identity ident") .append(" inner join fetch ident.user identUser") .append(" where box.resName=:resName and box.resId=:resId"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and box.resSubPath=:resSubPath"); } boolean hasBusinessGroups = businessGroups != null && businessGroups.size() > 0; if(hasBusinessGroups) { sb.append(" and "); if(re != null) { sb.append(" ( "); } sb.append(" check.identity.key in ( select membership.identity.key from bgroupmember membership ") .append(" where membership.group in (:baseGroups) and membership.role='").append(GroupRole.participant).append("'") .append(" )"); } if(re != null) { if(hasBusinessGroups) { sb.append(" or "); } else { sb.append(" and "); } sb.append(" check.identity.key in ( select membership.identity.key from repoentrytogroup as rel, bgroup as reBaseGroup, bgroupmember membership ") .append(" where rel.entry.key=:repoKey and rel.group=reBaseGroup and membership.group=reBaseGroup and membership.role='").append(GroupRole.participant).append("'") .append(" )"); if(hasBusinessGroups) { sb.append(" ) "); } } TypedQuery<DBCheck> query = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), DBCheck.class) .setParameter("resName", ores.getResourceableTypeName()) .setParameter("resId", ores.getResourceableId()); if(StringHelper.containsNonWhitespace(resSubPath)) { query.setParameter("resSubPath", resSubPath); } if(hasBusinessGroups) { List<Group> groups = new ArrayList<>(businessGroups.size()); for(BusinessGroup businessGroup:businessGroups) { groups.add(businessGroup.getBaseGroup()); } query.setParameter("baseGroups", groups); } if(re != null) { query.setParameter("repoKey", re.getKey()); } List<DBCheck> checks = query.getResultList(); Map<Long, AssessmentData> identToBox = new HashMap<Long,AssessmentData>(); for(DBCheck check:checks) { AssessmentData data = identToBox.get(check.getIdentity().getKey()); if(data == null) { data = new AssessmentData(check.getIdentity()); identToBox.put(check.getIdentity().getKey(), data); } data.getChecks().add(check); } return new ArrayList<AssessmentData>(identToBox.values()); } @Override public VFSContainer getFileContainer(CourseEnvironment courseEnv, CheckListCourseNode cNode) { String path = courseEnv.getCourseBaseContainer().getRelPath() + "/" + CheckListCourseNode.FOLDER_NAME + "/" + cNode.getIdent(); OlatRootFolderImpl rootFolder = new OlatRootFolderImpl(path, null); return rootFolder; } @Override public File getFileDirectory(CourseEnvironment courseEnv, CheckListCourseNode cNode) { String path = courseEnv.getCourseBaseContainer().getRelPath() + "/" + CheckListCourseNode.FOLDER_NAME + "/" + cNode.getIdent(); File rootFolder = new File(FolderConfig.getCanonicalRoot(), path); return rootFolder; } private static class BatchComparator implements Comparator<AssessmentBatch> { @Override public int compare(AssessmentBatch o1, AssessmentBatch o2) { Long id1 = o1.getIdentityKey(); Long id2 = o2.getIdentityKey(); return id1.compareTo(id2); } } }