/**
* 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) 1999-2009 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.util.logging.activity;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.List;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.ILoggingResourceable;
import org.olat.core.logging.activity.ILoggingResourceableType;
import org.olat.core.logging.activity.OlatResourceableType;
import org.olat.core.logging.activity.StringResourceableType;
import org.olat.core.util.resource.OresHelper;
import org.olat.course.ICourse;
import org.olat.course.groupsandrights.CourseGroupManager;
import org.olat.course.nodes.CourseNode;
import org.olat.group.BusinessGroup;
import org.olat.group.area.BGArea;
import org.olat.group.context.BGContext;
import org.olat.group.ui.run.BusinessGroupMainRunController;
import org.olat.modules.fo.Forum;
import org.olat.modules.fo.ForumManager;
import org.olat.modules.fo.Message;
import org.olat.modules.webFeed.models.Feed;
import org.olat.modules.webFeed.models.Item;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryManager;
import org.olat.resource.OLATResource;
/**
* A LoggingResourceable is the least common denominator between an OlatResourceable,
* an OlatResource, a RepositoryEntry and simple Strings - all of which want to be
* used as (greatGrandParent,grandParent,parent,target) resourcs in the logging table.
* <p>
* The idea of this class is to have one class containing the three fields
* <ul>
* <li>type: what sort of resource is it</li>
* <li>id: an id of the olat database - if available</li>
* <li>name: some sort of name or title of this resource</li>
* </ul>
* combined.
* <p>
* Besides the above (container for the triple type/id/name) it serves the purpose
* of doing checks between the businessPath/contextEntries and the ThreadLocalUserActivityLogger's
* LoggingResourceables which have been collected all the way from the initial request
* creating a particular Controller to the actual event handling method calling
* into IUserActivityLogger.log() - optionally passing additional LoggingResourceables.
* <p>
* The above check is done as a testing means to assure the data we're logging
* matches what we expect it to contain.
* <p>
* This way we avoid difficult if not unrealistic testing of the use of this
* IUserActivityLogging framework.
* <p>
* If a comparison with the businessPath fails, a simple (technical) log.WARN is issued.
* This should then be noticed by the system administrator hence feeding back
* into a patch or a fix for the next release.
* <P>
* Initial Date: 20.10.2009 <br>
* @author Stefan
*/
public class LoggingResourceable implements ILoggingResourceable {
/** the logging object used in this class **/
private static final OLog log_ = Tracing.createLoggerFor(LoggingResourceable.class);
private static final String DEFAULT_COURSE_GROUP_CONTEXT_NAME = "Default Course Group Context";
/** the maximum number of bytes for the name field **/
public static final int MAX_NAME_LEN = 240;
/** the maximum number of bytes for the id field **/
public static final int MAX_ID_LEN = 60;
/** the maximum number of bytes for the type field **/
public static final int MAX_TYPE_LEN = 30;
/** type of this LoggingResourceable - contains the OlatResourceable's type in the OlatResourceable case,
* or the enum name() of the StringResourceableType otherwise
*/
private final String type_;
/** the id of this LoggingResourceable - contains the OlatResource or RepositoryEntry's ID in those cases,
* or -1 in the StringResourceableType case.
*/
private final String id_;
/** the name of this LoggingResourceable - this can be the title in case of a course - or
* the html name of a page in case of cp
*/
private final String name_;
/** the ILoggingResourceableType corresponding to this LoggingResourceable - this is used for
* checks against the businessPath
*/
private final ILoggingResourceableType resourceableType_;
/** the OlatResourceable if we have one - null otherwise. Used for equals() and the businessPath check mainly **/
private final OLATResourceable resourceable_;
/**
* Restrict the given argument to the given number of bytes using UTF-8 encoding.
* <p>
* This method does not issue any logging
* @param arg the string to be size-restricted
* @param maxBytes the maximum number of bytes the string should result to when converting into UTF-8
* @return a string matching into the given number of bytes
*/
public static String restrictStringLength(String arg, int maxBytes) {
return restrictStringLength(arg, maxBytes, null, false);
}
/**
* Utility method to restrict the given String 'arg' to be of byte-length 'maxBytes'.
* <p>
* Uses String.getBytes() to determine byte length
* @param arg the String to be restricted to the maxBytes length
* @param maxBytes the max length allowed
* @param argNameForLogging the name of the arg value - used in case there's an error to give more accurate logging details
* @return
*/
private static String restrictStringLength(String arg, int maxBytes, String argNameForLogging, boolean log) {
if (arg==null) {
// we don't restrict arg not to be null - in this case we just return null
return null;
}
// otherwise, if arg is not null, then we check its length
try{
if (arg.getBytes("UTF-8").length<=maxBytes) {
// all fine
return arg;
}
if (log) log_.error("restrictStringLength: "+argNameForLogging+" too long. Allowed "+maxBytes+", actual: "+arg.getBytes().length+", value="+arg);
String result = arg.substring(0, Math.min(arg.length()-1, maxBytes));
while(result.getBytes("UTF-8").length>maxBytes) {
result = result.substring(0, result.length()-4);
}
return result;
} catch(UnsupportedEncodingException uee) {
log_.error("restrictStringLength: unsupported encoding: ", uee);
if (arg.getBytes().length<=maxBytes) {
// all fine
return arg;
}
if (log) log_.error("restrictStringLength: "+argNameForLogging+" too long. Allowed "+maxBytes+", actual: "+arg.getBytes().length+", value="+arg);
String result = arg.substring(0, Math.min(arg.length()-1, maxBytes));
while(result.getBytes().length>maxBytes) {
result = result.substring(0, result.length()-4);
}
return result;
}
}
/**
* Internal constructor to create a LoggingResourceable object with the given mandatory
* parameters initialized.
* <p>
* This method also does length checks to catch oversized parameters as early as possible
* (versus later in the hibernate/mysql handling)
* <p>
* @param resourceable the OlatResourceable if available - can be null
* @param resourceableType the type which is used for comparison later during businessPath checks
* @param type the type to be stored to the database
* @param id the id to be stored to the database
* @param name the name to be stored to the database
*/
private LoggingResourceable(OLATResourceable resourceable, ILoggingResourceableType resourceableType, String type, String id, String name) {
type_ = restrictStringLength(type, MAX_TYPE_LEN, "type", true);
id_ = restrictStringLength(id, MAX_ID_LEN, "id", true);
name_ = restrictStringLength(name, MAX_NAME_LEN, "name", true);
resourceable_ = resourceable;
resourceableType_ = resourceableType;
}
//
// Following is a set of wrap*() methods which take specific 'olat resourceable' objects
// and selects the type/id/name information to be taken out of it
//
public static LoggingResourceable wrapScormRepositoryEntry(RepositoryEntry scormRepoEntry) {
if (scormRepoEntry==null) {
throw new IllegalArgumentException("scormRepoEntry must not be null");
}
return wrap(scormRepoEntry, OlatResourceableType.scormResource);
}
/**
* Wraps a Wiki into a LoggingResourceable
* @param olatResourceable the wiki
* @return a LoggingResourceable representing the given wiki
*/
public static LoggingResourceable wrapWikiOres(OLATResourceable olatResourceable) {
if (olatResourceable==null) {
throw new IllegalArgumentException("olatResourceable must not be null");
}
if (olatResourceable.equals(BusinessGroupMainRunController.ORES_TOOLWIKI)) {
return new LoggingResourceable(olatResourceable, OlatResourceableType.wiki, "wiki", "0", "");
} else {
return wrap(olatResourceable, OlatResourceableType.wiki);
}
}
/**
* General wrapper for an OlatResourceable - as it's not obvious of what type that
* OlatResourceable is (in terms of being able to later compare it against the businessPath etc)
* an ILoggingResourceableType needs to be passed to this method as well.
* @param olatResourceable a general OlatResourceable
* @param type the type of the olatResourceable
* @return a LoggingResourceable wrapping the given olatResourceable type pair
*/
public static LoggingResourceable wrap(OLATResourceable olatResourceable, ILoggingResourceableType type) {
RepositoryEntry repoEntry = null;
if (olatResourceable instanceof RepositoryEntry) {
repoEntry = (RepositoryEntry) olatResourceable;
} else {
repoEntry = RepositoryManager.getInstance().lookupRepositoryEntry(olatResourceable, false);
}
if (repoEntry!=null) {
return new LoggingResourceable(repoEntry, type, repoEntry.getOlatResource().getResourceableTypeName(),
String.valueOf(repoEntry.getOlatResource().getResourceableId()), repoEntry.getDisplayname());
} else if (olatResourceable instanceof OLATResource) {
OLATResource olatResource = (OLATResource) olatResourceable;
return new LoggingResourceable(olatResource, type, olatResource.getResourceableTypeName(),
String.valueOf(olatResource.getResourceableId()), String.valueOf(olatResource.getKey()));
} else {
return new LoggingResourceable(olatResourceable, type, olatResourceable.getResourceableTypeName(),
String.valueOf(olatResourceable.getResourceableId()), "");
}
}
/**
* General wrapper for non OlatResourceable types - i.e. for simple Strings.
* <p>
* The LoggingResourceable always needs to have an ILoggingResourceableType - therefore
* it needs to be passed to this method.
* <p>
* Note that the typeForDB (so to speak) is set to ILoggingResourceableType.name().
* <p>
* Also note that there are a few further specialized wrapXXX(String) methods for
* selected StringResourceableTypes.
* <p>
* @param type the ILoggingResourceableType which corresponds the given id/name information
* @param idForDB the id - to be stored to the database
* @param nameForDB the name - to be stored to the database
* @return a LoggingResourceable wrapping the given type/id/name triple
*/
public static LoggingResourceable wrapNonOlatResource(StringResourceableType type, String idForDB, String nameForDB) {
return new LoggingResourceable(null, type,
type.name(), idForDB, nameForDB);
}
/**
* Wraps a filename as type StringResourceableType.uploadFile into a LoggingResourceable
* @param uploadFileName the filename - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given filename as type StringResourceableType.uploadFile
*/
public static LoggingResourceable wrapUploadFile(String uploadFileName) {
return wrapNonOlatResource(StringResourceableType.uploadFile, createUniqueId(StringResourceableType.uploadFile.toString(), uploadFileName), uploadFileName);
}
/**
* Wraps a filename as type StringResourceableType.bcFile into a LoggingResourceable
* @param bcFileName the filename - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given filename as type StringResourceableType.bcFile
*/
public static LoggingResourceable wrapBCFile(String bcFileName) {
return wrapNonOlatResource(StringResourceableType.bcFile, createUniqueId(StringResourceableType.bcFile.toString(), bcFileName), bcFileName);
}
/**
* Wraps a cpNodeName as type StringResourceableType.cpNode into a LoggingResourceable
* @param cpNodeName the node name - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given node name as type StringResourceableType.cpNode
*/
public static LoggingResourceable wrapCpNode(String cpNodeName) {
return wrapNonOlatResource(StringResourceableType.cpNode, createUniqueId(StringResourceableType.cpNode.toString(), cpNodeName), cpNodeName);
}
/**
* Wraps a single page uri as type StringResourceableType.spUri into a LoggingResourceable
* @param spUri the single page uri - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given uri as type StringResourceableType.spUri
*/
public static LoggingResourceable wrapSpUri(String spUri) {
return wrapNonOlatResource(StringResourceableType.spUri, createUniqueId(StringResourceableType.spUri.toString(), spUri), spUri);
}
/**
* Wraps a businessgroup right as type StringResourceableType.bgRight into a LoggingResourceable
* @param right the name of the businessgroup right - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given right name as type StringResourceableType.bgRight
*/
public static LoggingResourceable wrapBGRight(String right) {
return wrapNonOlatResource(StringResourceableType.bgRight, createUniqueId(StringResourceableType.bgRight.toString(), right), right);
}
/**
* Wraps a filename of type StringResourceableType.uploadFile into a LoggingResourceable
* @param uploadFileName the filename - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given filename as type StringResourceableType.uploadFile
*/
public static LoggingResourceable wrap(BGArea bgArea) {
return wrapNonOlatResource(StringResourceableType.bgArea, createUniqueId(StringResourceableType.bgArea.toString(), bgArea.toString()), bgArea.getName());
}
/**
* Wraps an Identity as type StringResourceableType.targetIdentity into a LoggingResourceable
* @param identity the identity - to be stored to the database in the name field
* @return a LoggingResourceable wrapping the given identity as type StringResourceableType.targetIdentity
*/
public static LoggingResourceable wrap(Identity identity) {
return wrapNonOlatResource(StringResourceableType.targetIdentity, String.valueOf(identity.getKey()), identity.getName());
}
/**
* Wraps a Forum into a LoggingResourceable - setting type/id/name accordingly
* @param forum the forum to be wrapped
* @return a LoggingResourceable wrapping the given Forum
*/
public static LoggingResourceable wrap(Forum forum) {
final String name;
List<Message> forumMessages = ForumManager.getInstance().getMessagesByForum(forum);
if (forumMessages==null || forumMessages.size()==0) {
name = null;
} else {
name = forumMessages.get(0).getTitle();
}
return new LoggingResourceable(forum, OlatResourceableType.forum, forum.getResourceableTypeName(),
String.valueOf(forum.getResourceableId()), name);
}
/**
* Wraps a (Forum) Message into a LoggingResourceable - setting type/id/name accordingly
* @param message the message to be wrapped
* @return a LoggingResourceable wrapping the given (Forum) Message
*/
public static LoggingResourceable wrap(Message forumMessage) {
return new LoggingResourceable(OresHelper.createOLATResourceableInstance(Message.class, forumMessage.getKey()), OlatResourceableType.forumMessage, OlatResourceableType.forumMessage.name(),
String.valueOf(forumMessage.getKey()), forumMessage.getTitle());
}
/**
* Wraps a Feed into a LoggingResourceable - setting type/id/name accordingly
* @param feed the feed to be wrapped
* @return a LoggingResourceable wrapping the given feed
*/
public static LoggingResourceable wrap(Feed feed) {
String title = feed.getTitle();
// truncate title after 230 chars
if (title.length() > 230) title = title.substring(0, 229);
return new LoggingResourceable(feed, OlatResourceableType.feed, feed.getResourceableTypeName(),
String.valueOf(feed.getResourceableId()), title);
}
/**
* Wraps a (Feed) Item into a LoggingResourceable - setting type/id/name accordingly
* @param item the item to be wrapped
* @return a LoggingResourceable wrapping the given (Feed) Item
*/
public static LoggingResourceable wrap(Item item) {
if (item.getExternalLink() != null) {
// external feeds often use URL's as Guid, but URL's are too long. Thus in this case use the name instead of the ID field.
String guid = item.getGuid();
// only use last 230 chars of the URL if too long
if (guid.length() > 230) guid = guid.substring(guid.length() - 230);
return wrapNonOlatResource(StringResourceableType.feedItem, null, guid);
} else {
String title = item.getTitle();
// truncate title after 230 chars
if (title.length() > 230) title = title.substring(0, 229);
return wrapNonOlatResource(StringResourceableType.feedItem, item.getGuid(), title);
}
}
/**
* Wraps a BGContext into a LoggingResourceable - setting type/id/name accordingly
* @param bgContext the bgContext to be wrapped
* @return a LoggingResourceable wrapping the given BGContext
*/
public static LoggingResourceable wrap(BGContext bgContext) {
String name = bgContext.getName();
if (name.startsWith(CourseGroupManager.DEFAULT_NAME_LC_PREFIX)) {
name = DEFAULT_COURSE_GROUP_CONTEXT_NAME;
}
return new LoggingResourceable(bgContext, OlatResourceableType.bgContext, bgContext.getGroupType(),
String.valueOf(bgContext.getResourceableId()), name);
}
/**
* Wraps a BusinessGroup into a LoggingResourceable - setting type/id/name accordingly
* @param group the group to be wrapped
* @return a LoggingResourceable wrapping the given BusinessGroup
*/
public static LoggingResourceable wrap(BusinessGroup group) {
return new LoggingResourceable(group, OlatResourceableType.businessGroup, group.getResourceableTypeName(),
String.valueOf(group.getKey()), group.getName());
}
/**
* Wraps a ICourse into a LoggingResourceable - setting type/id/name accordingly
* @param course the course to be wrapped
* @return a LoggingResourceable wrapping the given ICourse
*/
public static LoggingResourceable wrap(ICourse course) {
return new LoggingResourceable(course, OlatResourceableType.course, course.getResourceableTypeName(),
String.valueOf(course.getResourceableId()), course.getCourseTitle());
}
/**
* Wraps a CourseNode into a LoggingResourceable - setting type/id/name accordingly
* @param node the node to be wrapped
* @return a LoggingResourceable wrapping the given node
*/
public static LoggingResourceable wrap(CourseNode node) {
final String name = node.getShortTitle();
final String ident = node.getIdent();
final String typeForLogging = node.getType();
try{
Long id = Long.parseLong(ident);
return new LoggingResourceable(OresHelper.createOLATResourceableInstance("CourseNode", id), OlatResourceableType.node, typeForLogging,
node.getIdent(), name);
} catch(NumberFormatException nfe) {
return new LoggingResourceable(null, OlatResourceableType.node, typeForLogging,
node.getIdent(), name);
}
}
/**
* Create unique id.
* @param type
* @param uploadFileName
* @return
*/
private static String createUniqueId(String type, String name) {
return OresHelper.createStringRepresenting(OresHelper.createOLATResourceableType(type), name);
}
@Override
public String toString() {
return "LoggingResourceInfo[type="+type_+",rtype="+resourceableType_.name()+",id="+id_+",name="+name_+"]";
}
/**
* Returns the type of this LoggingResourceable - this is the OlatResourceable's type
* (in case this LoggingResource represents a OlatResourceable) - or the StringResourceableType's enum name()
* otherwise
* @return the type of this LoggingResourceable
*/
public String getType() {
return type_;
}
/**
* Returns the id of this LoggingResourceable - the id varies depending on the type of this
* LoggingResourceable - but usually it is the olatresourceable id or the olatresource id.
* @return the id of this LoggingResourceable
*/
public String getId() {
return id_;
}
/**
* Returns the name of this LoggingResourceable - the name varies depending on the type
* of this LoggingResource - e.g. in the course case it is the name of the course, in
* the CP case it is the html filename incl path
* @return
*/
public String getName() {
return name_;
}
/**
* Returns the ILoggingResourceableType of this LoggingResourceable - used for businessPath checking
* @return the ILoggingResourceableType of this LoggingResourceable
*/
public ILoggingResourceableType getResourceableType() {
return resourceableType_;
}
@Override
public int hashCode() {
return type_.hashCode()+(id_!=null ? id_.hashCode() : 1)+(resourceable_!=null ? resourceable_.getResourceableTypeName().hashCode()+(int)resourceable_.getResourceableId().longValue() : 0) + (resourceableType_!=null ? resourceableType_.hashCode() : 0);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LoggingResourceable)) {
return false;
} else if (super.equals(obj)) {
return true;
} else if (hashCode()!=obj.hashCode()) {
return false;
}
LoggingResourceable lri = (LoggingResourceable)obj;
if (!type_.equals(lri.type_)) {
return false;
}
if (id_==null) {
if (lri.id_!=null) {
return false;
}
} else if (lri.id_==null) {
return false;
} else if (!id_.equals(lri.id_)) {
return false;
}
if (resourceableType_!=lri.resourceableType_) {
return false;
}
if (resourceable_==null && lri.resourceableType_!=null) {
return false;
}
if (resourceable_!=null && lri.resourceableType_==null) {
return false;
}
if (!resourceable_.getResourceableTypeName().equals(lri.resourceable_.getResourceableTypeName())) {
return false;
}
if (!resourceable_.getResourceableId().equals(lri.resourceable_.getResourceableId())) {
return false;
}
// bingo
return true;
}
/**
* Checks whether this LoggingResourceable represents the same resource as the
* given ContextEntry.
* <p>
* This is used during the businessPath check.
* @param ce
* @return
*/
public boolean correspondsTo(ContextEntry ce) {
if (ce==null) {
return false;
}
OLATResourceable ceResourceable = ce.getOLATResourceable();
if (ceResourceable==null) {
return false;
}
if (resourceable_!=null) {
if (ceResourceable.getResourceableTypeName().equals(resourceable_.getResourceableTypeName()) &&
ceResourceable.getResourceableId().equals(resourceable_.getResourceableId())) {
return true;
}
if (ceResourceable instanceof RepositoryEntry) {
RepositoryEntry re = (RepositoryEntry) ceResourceable;
OLATResource ores = re.getOlatResource();
if (ores!=null &&
ores.getResourceableTypeName().equals(resourceable_.getResourceableTypeName()) &&
ores.getResourceableId().equals(resourceable_.getResourceableId())) {
return true;
}
} else if (OresHelper.calculateTypeName(RepositoryEntry.class).equals(ceResourceable.getResourceableTypeName())) {
// @TODO: Performance hit! Speed optimize this!
// OLAT-4996
// OLAT-4955
// that's the jump-in case where the ContextEntry says it has a [RepositoryEntry:123212321] but
// the actual class of ceResourceable is not a RepositoryEntry but an OresHelper$3 ...
// in which case all we have is the key of the repositoryentry and we must make a DB lookup to
// map the repo key to the corresponding olatresource
RepositoryEntry re = RepositoryManager.getInstance().lookupRepositoryEntry(ceResourceable.getResourceableId());
if (re!=null) {
OLATResource ores = re.getOlatResource();
if (ores!=null &&
ores.getResourceableTypeName().equals(resourceable_.getResourceableTypeName()) &&
ores.getResourceableId().equals(resourceable_.getResourceableId())) {
return true;
}
}
}
return ceResourceable.equals(resourceable_);
}
// if resourceable_ is null it's rather difficult to compare us with the contextentry
// we still try...
if (type_.equals(StringResourceableType.targetIdentity.name()) &&
ceResourceable.getResourceableTypeName()=="Identity") {
return id_.equals(String.valueOf(ceResourceable.getResourceableId()));
}
return false;
}
}