/**
* <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.portfolio.manager;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
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 java.util.UUID;
import org.olat.basesecurity.IdentityRef;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBQuery;
import org.olat.core.commons.services.tagging.manager.TaggingManager;
import org.olat.core.commons.services.tagging.model.Tag;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.AssertException;
import org.olat.core.manager.BasicManager;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.portfolio.EPArtefactHandler;
import org.olat.portfolio.PortfolioModule;
import org.olat.portfolio.model.EPFilterSettings;
import org.olat.portfolio.model.artefacts.AbstractArtefact;
import org.olat.portfolio.model.structel.EPStructureToArtefactLink;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* Description:<br>
* EPArtefactManager manage the artefacts
*
* <P>
* Initial Date: 11.06.2010 <br>
*
* @author Roman Haag, roman.haag@frentix.com, http://www.frentix.com
*/
@Service("epArtefactManager")
public class EPArtefactManager extends BasicManager {
private static final String ARTEFACT_FULLTEXT_ON_FS = "ARTEFACT_FULLTEXT_ON_FS";
@Autowired
private DB dbInstance;
@Autowired
private PortfolioModule portfolioModule;
@Autowired
private TaggingManager taggingManager;
private static final int ARTEFACT_FULLTEXT_DB_FIELD_LENGTH = 16384;
public static final String ARTEFACT_CONTENT_FILENAME = "artefactContent.html";
private static final String ARTEFACT_INTERNALDATA_FOLDER = "data";
private VFSContainer artefactsRoot;
public EPArtefactManager() {
//
}
/**
* Used by the indexer to retrieve all the artefacts
* @param artefactIds List of ids to seek (optional)
* @param firstResult First position
* @param maxResults Max number of returned artefacts (0 or below for all)
* @return
*/
@SuppressWarnings("unchecked")
protected List<AbstractArtefact> getArtefacts(Identity author, List<Long> artefactIds, int firstResult, int maxResults) {
StringBuilder sb = new StringBuilder();
sb.append("select artefact from ").append(AbstractArtefact.class.getName()).append(" artefact");
boolean where = false;
if(author != null) {
where = true;
sb.append(" where artefact.author=:author");
}
if(artefactIds != null && !artefactIds.isEmpty()) {
if(where) sb.append(" and ");
else sb.append(" where ");
sb.append(" artefact.id in (:artefactIds)");
}
DBQuery query = dbInstance.createQuery(sb.toString());
if(maxResults > 0) {
query.setMaxResults(maxResults);
}
if(firstResult >= 0) {
query.setFirstResult(firstResult);
}
if(author != null) {
query.setEntity("author", author);
}
if(artefactIds != null && !artefactIds.isEmpty()) {
query.setParameterList("artefactIds", artefactIds);
}
List<AbstractArtefact> artefacts = query.list();
return artefacts;
}
protected boolean isArtefactClosed(AbstractArtefact artefact) {
StringBuilder sb = new StringBuilder();
sb.append("select count(link) from ").append(EPStructureToArtefactLink.class.getName()).append(" link ")
.append(" inner join link.structureElement structure ")
.append(" inner join structure.root rootStructure")
.append(" where link.artefact=:artefact and rootStructure.status='closed'");
DBQuery query = dbInstance.createQuery(sb.toString());
query.setEntity("artefact", artefact);
Number count = (Number)query.uniqueResult();
return count.intValue() > 0;
}
protected boolean hasArtefactPool(IdentityRef ident) {
StringBuilder sb = new StringBuilder();
sb.append("select artefact.key from ").append(AbstractArtefact.class.getName()).append(" artefact").append(" where author.key=:authorKey");
List<Long> firstKey = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Long.class)
.setParameter("authorKey", ident.getKey())
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
return firstKey != null && firstKey.size() > 0 && firstKey.get(0) != null && firstKey.get(0).longValue() >= 0;
}
protected List<AbstractArtefact> getArtefactPoolForUser(Identity ident) {
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
sb.append("select artefact from ").append(AbstractArtefact.class.getName()).append(" artefact").append(" where author=:author");
DBQuery query = dbInstance.createQuery(sb.toString());
query.setEntity("author", ident);
@SuppressWarnings("unchecked")
List<AbstractArtefact> artefacts = query.list();
if (artefacts.isEmpty()) return null;
long duration = System.currentTimeMillis() - start;
if (isLogDebugEnabled()) logDebug("loading the full artefact pool took " + duration + "ms");
return artefacts;
}
protected VFSContainer getArtefactsRoot() {
if (artefactsRoot == null) {
VFSContainer root = portfolioModule.getPortfolioRoot();
VFSItem artefactsItem = root.resolve("artefacts");
if (artefactsItem == null) {
artefactsRoot = root.createChildContainer("artefacts");
} else if (artefactsItem instanceof VFSContainer) {
artefactsRoot = (VFSContainer) artefactsItem;
} else {
logError("The root folder for artefact is a file and not a folder", null);
}
}
return artefactsRoot;
}
protected VFSContainer getArtefactsTempContainer(Identity ident){
VFSContainer artRoot = new OlatRootFolderImpl(File.separator + "tmp", null);
VFSItem tmpI = artRoot.resolve("portfolio");
if (tmpI == null) {
tmpI = artRoot.createChildContainer("portfolio");
}
VFSItem userTmp = tmpI.resolve(ident.getName());
if (userTmp == null){
userTmp = ((VFSContainer) tmpI).createChildContainer(ident.getName());
}
String idFolder = UUID.randomUUID().toString();
VFSContainer thisTmp = ((VFSContainer) userTmp).createChildContainer(idFolder);
return thisTmp;
}
protected List<String> getArtefactTags(AbstractArtefact artefact) {
// wrap concrete artefact as abstract-artefact to get the correct resName for the tag
if (artefact.getKey() == null ) return null;
OLATResourceable artefactOres = OresHelper.createOLATResourceableInstance(AbstractArtefact.class, artefact.getKey());
List<String> tags = taggingManager.getTagsAsString(null, artefactOres, null, null);
return tags;
}
protected void setArtefactTag(Identity identity, AbstractArtefact artefact, String tag) {
// wrap concrete artefact as abstract-artefact to get the correct resName for the tag
OLATResourceable artefactOres = OresHelper.createOLATResourceableInstance(AbstractArtefact.class, artefact.getKey());
taggingManager.createAndPersistTag(identity, tag, artefactOres, null, null);
}
protected void setArtefactTags(Identity identity, AbstractArtefact artefact, List<String> tags) {
if (tags==null) return;
// wrap concrete artefact as abstract-artefact to get the correct resName for the tag
OLATResourceable artefactOres = OresHelper.createOLATResourceableInstance(AbstractArtefact.class, artefact.getKey());
List<Tag> oldTags = taggingManager.loadTagsForResource(artefactOres, null, null);
List<String> oldTagStrings = new ArrayList<String>();
List<String> tagsToAdd = new ArrayList<String>(tags.size());
tagsToAdd.addAll(tags);
if (oldTags != null) { // there might be no tags yet
for (Tag oTag : oldTags) {
if (tags.contains(oTag.getTag())){
// still existing, nothing to do
oldTagStrings.add(oTag.getTag());
tagsToAdd.remove(oTag.getTag());
} else {
// tag was deleted, remove it
taggingManager.deleteTag(oTag);
}
}
}
// look for all given tags, add the ones yet missing
for (String tag : tagsToAdd) {
if (StringHelper.containsNonWhitespace(tag)) {
taggingManager.createAndPersistTag(identity, tag, artefactOres, null, null);
}
}
}
/**
* Create and persist an artefact of the given type
*
* @param type
* @return The persisted artefact
*/
protected AbstractArtefact createAndPersistArtefact(Identity identity, String type) {
EPArtefactHandler<?> handler = portfolioModule.getArtefactHandler(type);
if(handler != null && handler.isEnabled()){
AbstractArtefact artefact = handler.createArtefact();
artefact.setAuthor(identity);
dbInstance.saveObject(artefact);
saveArtefactFulltextContent(artefact);
return artefact;
} else {
return null;
}
}
protected AbstractArtefact updateArtefact(AbstractArtefact artefact) {
if (artefact == null) return null;
String tmpFulltext = artefact.getFulltextContent();
if (StringHelper.containsNonWhitespace(tmpFulltext) && artefact.getFulltextContent().equals(ARTEFACT_FULLTEXT_ON_FS)){
tmpFulltext = getArtefactFullTextContent(artefact);
}
artefact.setFulltextContent("");
if (artefact.getKey() == null) {
dbInstance.saveObject(artefact);
} else {
dbInstance.updateObject(artefact);
}
artefact.setFulltextContent(tmpFulltext);
saveArtefactFulltextContent(artefact);
return artefact;
}
// decides itself if fulltext fits into db or will be written on fs
protected boolean saveArtefactFulltextContent(AbstractArtefact artefact){
String fullText = artefact.getFulltextContent();
if (StringHelper.containsNonWhitespace(fullText)) {
if (fullText.length() > ARTEFACT_FULLTEXT_DB_FIELD_LENGTH){
// save the real content on FS
try {
VFSContainer container = getArtefactContainer(artefact);
VFSLeaf artData = (VFSLeaf) container.resolve(ARTEFACT_CONTENT_FILENAME);
if (artData == null) {
artData = container.createChildLeaf(ARTEFACT_CONTENT_FILENAME);
}
VFSManager.copyContent(new ByteArrayInputStream(fullText.getBytes()), artData, true);
artefact.setFulltextContent(ARTEFACT_FULLTEXT_ON_FS);
dbInstance.updateObject(artefact);
} catch (Exception e) {
logError("could not really save the fulltext content of an artefact", e);
return false;
}
} else {
// if length is shorter, but still a file there -> delete it (but only if loading included the long version from fs before, else its overwritten!)
VFSLeaf artData = (VFSLeaf) getArtefactContainer(artefact).resolve(ARTEFACT_INTERNALDATA_FOLDER + "/" + ARTEFACT_CONTENT_FILENAME); // v.1 had /data/ in path
if (artData!=null) artData.delete();
artData = (VFSLeaf) getArtefactContainer(artefact).resolve(ARTEFACT_CONTENT_FILENAME);
if (artData!=null) artData.delete();
dbInstance.updateObject(artefact); // persist fulltext in db
}
}
return true;
}
protected String getArtefactFullTextContent(AbstractArtefact artefact){
VFSLeaf artData = (VFSLeaf) getArtefactContainer(artefact).resolve(ARTEFACT_CONTENT_FILENAME);
if (artData== null) artData = (VFSLeaf) getArtefactContainer(artefact).resolve(ARTEFACT_INTERNALDATA_FOLDER + "/" + ARTEFACT_CONTENT_FILENAME); // fallback to v.1
if (artData!=null) {
return FileUtils.load(artData.getInputStream(), "utf-8");
} else return artefact.getFulltextContent();
}
/**
* This is an optimized method to filter a list of artefact by tags and return
* the tags of this list of artefacts. This prevent to search two times or more the list
* of tags of an artefact.
* @param identity
* @param tags
* @return the filtered artefacts and their tags
*/
protected EPArtefactTagCloud getArtefactsAndTagCloud(Identity identity, List<String> tags) {
List<AbstractArtefact> artefacts = getArtefactPoolForUser(identity);
EPFilterSettings filterSettings = new EPFilterSettings();
filterSettings.setTagFilter(tags);
Set<String> newTags = new HashSet<String>();
filterArtefactsByTags(artefacts, filterSettings, newTags);
return new EPArtefactTagCloud(artefacts, newTags);
}
protected List<AbstractArtefact> filterArtefactsByFilterSettings(List<AbstractArtefact> allArtefacts, EPFilterSettings filterSettings) {
long start = System.currentTimeMillis();
if (allArtefacts == null) return null;
List<AbstractArtefact> filteredArtefactList = new ArrayList<AbstractArtefact>(allArtefacts.size());
filteredArtefactList.addAll(allArtefacts);
if (filterSettings != null && !filterSettings.isFilterEmpty()) {
if (filteredArtefactList.size() != 0) {
filterArtefactsByTags(filteredArtefactList, filterSettings, null);
}
if (filteredArtefactList.size() != 0) {
filterArtefactsByType(filteredArtefactList, filterSettings.getTypeFilter());
}
if (filteredArtefactList.size() != 0) {
filterArtefactsByString(filteredArtefactList, filterSettings.getTextFilter());
}
if (filteredArtefactList.size() != 0) {
filterArtefactsByDate(filteredArtefactList, filterSettings.getDateFilter());
}
}
long duration = System.currentTimeMillis() - start;
if (isLogDebugEnabled()) logDebug("filtering took " + duration + "ms");
return filteredArtefactList;
}
/**
* @param allArtefacts
* @param filterSettings (containing tags to filter for or boolean if filter should keep only artefacts without a tag)
* @param collect the tags found in the filtered artefacts
* @return filtered artefact list
*/
private void filterArtefactsByTags(List<AbstractArtefact> artefacts, EPFilterSettings filterSettings, Set<String> cloud) {
List<String> tags = filterSettings.getTagFilter();
// either search for artefacts with given tags, or such with no one!
List<AbstractArtefact> toRemove = new ArrayList<AbstractArtefact>();
if (tags != null && tags.size() != 0) {
// TODO: epf: RH: fix needed, as long as tags with uppercase initial are
// allowed!
for (AbstractArtefact artefact : artefacts) {
List<String> artefactTags = getArtefactTags(artefact);
if (!artefactTags.containsAll(tags)) {
toRemove.add(artefact);
} else if(cloud != null) {
cloud.addAll(artefactTags);
}
}
artefacts.removeAll(toRemove);
} else if (filterSettings.isNoTagFilterSet()) {
for (AbstractArtefact artefact : artefacts) {
if (!getArtefactTags(artefact).isEmpty()) {
toRemove.add(artefact);
}
}
artefacts.removeAll(toRemove);
}
}
private void filterArtefactsByType(List<AbstractArtefact> artefacts, List<String> type) {
if (type != null && type.size() != 0) {
List<AbstractArtefact> toRemove = new ArrayList<AbstractArtefact>();
for (AbstractArtefact artefact : artefacts) {
if (!type.contains(artefact.getResourceableTypeName())) {
toRemove.add(artefact);
}
}
artefacts.removeAll(toRemove);
}
}
/**
* date comparison will first set startDate to 00:00:00 and set endDate to
* 23:59:59 else there might be no results if start = end date. dateList must
* be set according to: dateList(0) = startDate dateList(1) = endDate
*/
private void filterArtefactsByDate(List<AbstractArtefact> artefacts, List<Date> dateList) {
if (dateList != null && dateList.size() != 0) {
if (dateList.size() == 2) {
Date startDate = dateList.get(0);
Date endDate = dateList.get(1);
Calendar cal = Calendar.getInstance();
if (startDate == null) {
cal.set(1970, 1, 1);
} else {
cal.setTime(startDate);
}
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
startDate = cal.getTime();
cal.setTime(endDate);
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
endDate = cal.getTime();
List<AbstractArtefact> toRemove = new ArrayList<AbstractArtefact>();
for (AbstractArtefact artefact : artefacts) {
Date creationDate = artefact.getCreationDate();
if (!(creationDate.before(endDate) && creationDate.after(startDate))) {
toRemove.add(artefact);
}
}
artefacts.removeAll(toRemove);
} else throw new AssertException("provided DateList must contain exactly two Date-objects");
}
}
private void filterArtefactsByString(List<AbstractArtefact> artefacts, String textFilter) {
if (StringHelper.containsNonWhitespace(textFilter)) {
List<AbstractArtefact> toRemove = new ArrayList<AbstractArtefact>();
for (AbstractArtefact artefact : artefacts) {
String textCompare = artefact.getTitle() + artefact.getDescription() + artefact.getFulltextContent();
if (!textCompare.toLowerCase().contains(textFilter.toLowerCase())) {
toRemove.add(artefact);
}
}
artefacts.removeAll(toRemove);
}
}
/**
* Load the artefact by its primary key
*
* @param key The primary key
* @return The artefact or null if nothing found
*/
protected AbstractArtefact loadArtefactByKey(Long key) {
if (key == null) throw new NullPointerException();
StringBuilder sb = new StringBuilder();
sb.append("select artefact from ").append(AbstractArtefact.class.getName()).append(" artefact").append(" where artefact=:key");
DBQuery query = dbInstance.createQuery(sb.toString());
query.setLong("key", key);
@SuppressWarnings("unchecked")
List<AbstractArtefact> artefacts = query.list();
// if not found, it is an empty list
if (artefacts.isEmpty()) return null;
return artefacts.get(0);
}
protected List<AbstractArtefact> loadArtefactsByBusinessPath(String businessPath, Identity author){
if (!StringHelper.containsNonWhitespace(businessPath)) return null;
StringBuilder sb = new StringBuilder();
sb.append("select artefact from ").append(AbstractArtefact.class.getName()).append(" artefact")
.append(" where artefact.businessPath=:bpath");
if (author != null) {
sb.append(" and artefact.author=:ident");
}
DBQuery query = dbInstance.createQuery(sb.toString());
query.setString("bpath", businessPath);
if (author != null) {
query.setEntity("ident", author);
}
@SuppressWarnings("unchecked")
List<AbstractArtefact> artefacts = query.list();
// if not found, it is an empty list
if (artefacts.isEmpty()) return null;
return artefacts;
}
public int countArtefacts(Identity identity) {
StringBuilder sb = new StringBuilder();
sb.append("select count(artefact) from ").append(AbstractArtefact.class.getName()).append(" artefact")
.append(" where artefact.author=:ident");
Number count = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Number.class)
.setParameter("ident", identity)
.getSingleResult();
return count == null ? 0: count.intValue();
}
protected Map<String,Long> loadNumOfArtefactsByStartingBusinessPath(String startOfBusinessPath, IdentityRef author) {
if (!StringHelper.containsNonWhitespace(startOfBusinessPath) || author == null) {
return Collections.emptyMap();
}
StringBuilder sb = new StringBuilder();
sb.append("select artefact.businessPath, count(artefact.key) from ").append(AbstractArtefact.class.getName()).append(" artefact")
.append(" where artefact.businessPath like :bpath and artefact.author.key=:identityKey")
.append(" group by artefact.businessPath");
List<Object[]> objectsList = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class)
.setParameter("bpath", startOfBusinessPath + "%")
.setParameter("identityKey", author.getKey())
.getResultList();
Map<String,Long> stats = new HashMap<>();
for(Object[] objects:objectsList) {
String bp = (String)objects[0];
Long count = (Long)objects[1];
stats.put(bp, count);
}
return stats;
}
protected void deleteArtefact(AbstractArtefact artefact) {
getArtefactContainer(artefact).delete();
// wrap concrete artefact as abstract-artefact to get the correct resName for the tag
OLATResourceable artefactOres = OresHelper.createOLATResourceableInstance(AbstractArtefact.class, artefact.getKey());
taggingManager.deleteTags(artefactOres, null, null);
dbInstance.deleteObject(artefact);
logInfo("Deleted artefact " + artefact.getTitle() + " with key: " + artefact.getKey());
}
protected VFSContainer getArtefactContainer(AbstractArtefact artefact) {
Long key = artefact.getKey();
if (key == null) throw new AssertException("artefact not yet persisted -> no key available!");
VFSContainer container = null;
VFSItem item = getArtefactsRoot().resolve(key.toString());
if (item == null) {
container = getArtefactsRoot().createChildContainer(key.toString());
} else if (item instanceof VFSContainer) {
container = (VFSContainer) item;
} else {
logError("Cannot create a container for artefact: " + artefact, null);
}
return container;
}
}