/*
Copyright 2012 Christian Prause and Fraunhofer FIT
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package net.sf.collabreview.reputation;
import net.sf.collabreview.core.Artifact;
import net.sf.collabreview.core.ArtifactVisitor;
import net.sf.collabreview.core.Repository;
import net.sf.collabreview.core.configuration.ConfigurationData;
import net.sf.collabreview.core.filter.Filter;
import net.sf.collabreview.hooks.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.Serializable;
import java.util.*;
/**
* A ReputationMetric computes a user's (author, reviewer, ...) reputation in some way.
* They typically consider all Artifacts in the Repository.
* Examples for reputations could be: most active reviewer, best average quality, highest total score, ...
* ReputationMetrics are owned by the ReputationMetricManager.
* <p/>
* ReputationMetrics need not be fully initialized immediately after creation, i.e. scores do not have to be available.
* The ReputationMetricManager ensures that its metrics are registered with the ArtifactVisitor
* so they get updated whenever the ArtifactVisitor runs.
* Metrics may rely on the WorkerDaemon to periodically update their scores.
* When the metric has not yet computed the scores it's isReady() method returns <code>false</code>.
* <p/>
* A ReputationMetric uses a double-buffer for storing its results.
* The data in the old buffer will continue to be used until re-calculated scores are available.
* During the times at which the scores in the buffer are not upToDate, the upToDate flag is set to false.
* <p/>
* A ReputationMetric is associated with a Repository. Use the ReputationMetric <code>repository</code> field when
* implementing a ReputationMetric.
*
* @author Christian Prause (chris)
* @date 2010-07-31 11:04:18
* @see net.sf.collabreview.reputation.ReputationMetricManager
*/
public abstract class ReputationMetric implements Hookable {
/**
* Apache commons logging logger for class ReputationMetric.
*/
private static final Log logger = LogFactory.getLog(ReputationMetric.class);
/**
* The name of the metric which should be a short and simple identifier.
* It is either set in the configuration file or the default from the getDefaultName() method is used.
*/
private String name = getDefaultName();
/**
* Flag to check if the scores computed by metric are ready.
* Scores may not be ready if the initial calculation has not been finished yet.
*/
private boolean scoresReady;
/**
* Flag to check if data is upToDate.
* The flag signals to the ReputationMetricManager that a recalculation is necessary.
*/
private boolean upToDate;
/**
* Flag that is set iff an update (recalculation) of the scores is in progress.
*/
private boolean updateInProgress;
/**
* If not null it will exclude resources from the metric calculation.
* In case it is null, the ReputationMetrics defaultFilter is used.
*
* @see net.sf.collabreview.reputation.ReputationMetric#getFilter()
*/
private Filter filter;
/**
* Reputation scores for each author.
*/
private Map<String, Float> authorScores;
/**
* If this ReputationMetric is managed by an ReputationMetricManager, then this manager is stored here.
* If not, then this value is null.
*/
private ReputationMetricManager owner;
/**
* The VisitListener of this ReputationMetric.
*
* @see ReputationMetric#getVisitListener()
*/
private ArtifactVisitor.VisitListener theVisitListener = new ArtifactVisitor.VisitListener() {
@Override
public void beginVisiting(Repository repository) {
assert repository == ReputationMetric.this.getRepository();
updateInProgress = true;
ReputationMetric.this.beginVisiting();
}
@Override
public Filter getVisitFilter() {
return getFilter();
}
@Override
public void visit(Artifact artifact) {
ReputationMetric.this.visit(artifact);
}
@Override
public void endVisiting() {
setAuthorScores(ReputationMetric.this.endVisiting());
updateInProgress = false;
}
};
/**
* Manages the hook lists for a ReputationMetric object.
*/
private NotificationListManager<ReputationMetric> notificationListManager = new NotificationListManager<ReputationMetric>(
new NotificationList<ReputationMetricUpdatedHook, ReputationMetric>(this) {
@Override
protected void issueNotification(ReputationMetricUpdatedHook observer, ReputationMetric reputationMetric, Object[] parameters) {
observer.scoreUpdated(reputationMetric);
}
}
);
/**
* Get the Repository that provides Article and Review information.
*
* @return the Repository with Article and Review information
*/
protected Repository getRepository() {
return getOwner().getCollabReview().getRepository();
}
/**
* @return the ReputationMetricManager that owns this ReputationMetric, or null if this ReputationMetric is not owned by an ReputationMetricManager
* @see net.sf.collabreview.reputation.ReputationMetric#owner
*/
protected ReputationMetricManager getOwner() {
return owner;
}
/**
* Get the ReputationMetricManager that owns this ReputationMetric.
*
* @return the ReputationMetricManager that owns this ReputationMetric
*/
protected ReputationMetricManager getReputationMetricManager() {
return owner;
}
/**
* @param owner inject the ReputationMetricManager that owns this ReputationMetric, or null if this ReputationMetric is not owned by an ReputationMetricManager
* @see net.sf.collabreview.reputation.ReputationMetric#owner
*/
protected void setReputationMetricManager(ReputationMetricManager owner) {
this.owner = owner;
}
/**
* Get the filter that is used by this ReputationMetric.
* If there is no Filter defined for this ReputationMetric then the default one from the ReputationMetricManager
* will be used instead.
*
* @return the filter used by this ReputationMetric (which may be the defaultFilter defined by the ReputationMetricManager)
* or null if no filter is to be used
*/
protected Filter getFilter() {
if (filter == null) {
return owner.getDefaultFilter();
} else {
return filter;
}
}
/**
* Set the filter to use when artifacts are being visited.
*
* @param filter the new filter to use
*/
protected void setFilter(Filter filter) {
this.filter = filter;
}
/**
* Lets the ReputationMetric read its own configuration data.
* After reading some common elements like the filter, the configure() method is called.
*
* @param configurationData the configuration data
* @throws Exception if the configure() method of the metric throws an exception
* @see net.sf.collabreview.reputation.ReputationMetric#configure(net.sf.collabreview.core.configuration.ConfigurationData)
*/
protected final void startConfigure(ConfigurationData configurationData) throws Exception {
ConfigurationData filter = configurationData.getSubElement("filter");
if (filter != null) {
Filter f = Filter.readFilter(configurationData);
setFilter(f);
}
String name = configurationData.getValue("name");
if (name != null) {
this.name = name;
}
configure(configurationData);
}
/**
* Called by the ReputationMetricManager configure() method to initialize this ReputationMetric.
* The repository has been injected before this method is invoked.
*
* @param configuration the configuration data to use
* @throws Exception if something goes wrong...
*/
protected abstract void configure(ConfigurationData configuration) throws Exception;
/**
* Prepare this ReputationMetric for its destruction (e.g. remove registered listeners).
*/
protected abstract void destroy();
/**
* A ReputationMetric can have a VisitListener installed with the application's ArtifactVisitor.
* If a ReputationMetric wants to install a VisitListener it should return it when this method is invoked.
* The getVisitListener() will be invoked by the ReputationMetricManager;
* once when registering the VisitListener and once when it is removed when the ReputationMetricManager is destroyed.
* To allow for removal of the listener it is important that this method always returns the same VisitListener.
*
* @return the VisitListener this ReputationMetric want to have registered with the ArtifactVisitor, or null if this metric is not requiring a visit listener.
*/
protected ArtifactVisitor.VisitListener getVisitListener() {
return theVisitListener;
}
/**
* Informs this metric that CollabReview is now visiting all artifacts for a re-calculation of reputation scores.
* If the metric knows that nothing has changed, then it can ignore this information.
* Please note that the scoresReady flag is automatically set to false.
*
* @see net.sf.collabreview.reputation.ReputationMetric#visit(net.sf.collabreview.core.Artifact)
* @see net.sf.collabreview.reputation.ReputationMetric#endVisiting()
*/
protected abstract void beginVisiting();
/**
* CollabReview is now visiting all artifacts for which the filter does return true.
*
* @param artifact the artifact that is being visited
* @see net.sf.collabreview.reputation.ReputationMetric#beginVisiting()
*/
protected abstract void visit(Artifact artifact);
/**
* CollabReview has finished visiting all artifacts.
* This method should return the newly calculated reputation scores that then replace any previously cached values
* in the internal buffer of the metric.
* Note that through this the scoresReady and upToDate flags are automatically set to true.
*
* @return all the newly computed reputation values
* @see net.sf.collabreview.reputation.ReputationMetric#beginVisiting()
* @see net.sf.collabreview.reputation.ReputationMetric#scoresReady
* @see net.sf.collabreview.reputation.ReputationMetric#upToDate
*/
protected abstract Map<String, Float> endVisiting();
/**
* Some metrics might not be ready as soon as the object is created (probably because they wait for computation
* by the WorkerDaemon).
*
* @return true iff the metric has been fully initialized by having its scores computed
*/
public boolean isScoresReady() {
return scoresReady;
}
/**
* Check if the scores that this ReputationMetric returns are upToDate or if they need re-calculation.
*
* @return value of the upToDate flag
*/
public boolean isUpToDate() {
return upToDate;
}
/**
* Test if an update re-calculation is currently in progress.
* If an update is in progress, then ReputationMetrics should ignore any update notifications.
*
* @return true iff the re-calculation is currently running.
*/
public boolean isUpdateInProgress() {
return updateInProgress;
}
/**
* Set the value of the scoresReady flag.
*
* @param scoresReady new value for scoresReady
* @see net.sf.collabreview.reputation.ReputationMetric#scoresReady
*/
protected void setScoresReady(boolean scoresReady) {
this.scoresReady = scoresReady;
}
/**
* Set the upToDate flag.
*
* @param upToDate new value of the upToDate flag
*/
protected synchronized void setUpToDate(boolean upToDate) {
this.upToDate = upToDate;
if (upToDate) {
notifyAll();
}
}
/**
* @return an identifier string (few characters, only lower-case letters) for this metric.
*/
protected abstract String getDefaultName();
/**
* @return the name/key of this metric
*/
public String getName() {
return name;
}
/**
* Get the computed results for every user.
* The results are not sorted alphabetically but according to scores.
* The user with the highest number of points is first.
* <p/>
* If the scores are not yet ready, then this method throws an IllegalStateException.
*
* @return the reputation score for every author (the strings are author IDs).
* The results are in descending order.
* @see net.sf.collabreview.reputation.ReputationMetric#scoresReady
*/
public SortedMap<String, Float> getAuthorScores() {
if (!isScoresReady()) {
throw new IllegalStateException("Scores from metric \"" + getName() + "\" not ready");
}
SortedMap<String, Float> result = new TreeMap<String, Float>(new DescendingOrder());
result.putAll(authorScores);
return result;
}
/**
* Get the buffered reputation score for the respective user.
*
* @param user the user to obtain the score for
* @return the user's buffered reputation score, or null if there is no score value for this user
*/
public Float getAuthorScore(String user) {
if (!isScoresReady()) {
throw new IllegalStateException("Scores from metric \"" + getName() + "\" not ready");
}
return authorScores.get(user);
}
/**
* Get the ranking position the specified author has according to this metric.
*
* @param user the user for which to determine the ranking position
* @return the user's rank where 0 is the first place who scores the most points.
*/
public int getAuthorRank(String user) {
int betterCount = 0;
Float userScore = getAuthorScore(user);
if (userScore != null) {
for (String other : listContributors()) {
if (getAuthorScore(other) > userScore) {
betterCount++;
}
}
return betterCount;
} else {
return 0;
}
}
/**
* Update the author's score with a new value.
* This method is useful if an update of the cached scores can be done without having to fully recompute it.
*
* @param user the user who gets a new score
* @param score the user's new score
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void setAuthorScore(String user, float score) {
authorScores.put(user, score);
}
/**
* Switch the result cache to the newly calculated results.
* Sets the scoresReady and upToDate flags.
*
* @param authorScores the newly calculated authorScores that should now be cached.
* @see net.sf.collabreview.reputation.ReputationMetric#scoresReady
* @see net.sf.collabreview.reputation.ReputationMetric#upToDate
*/
protected void setAuthorScores(Map<String, Float> authorScores) {
logger.debug("Updating cached scores for \"" + getName() + "\"");
this.authorScores = authorScores;
setScoresReady(true);
setUpToDate(true);
notificationListManager.getNotificationList(ReputationMetricUpdatedHook.class).notifyObservers();
}
/**
* List the authors that have contributed and therefore have a reputation score.
* Will block if scoresReady flag is set to false.
* <p/>
* Contributors are sorted from most points to least.
* If both contributors have the same amount of points then they are sorted alphabetically.
*
* @return a collection with the contributors
*/
public List<String> listContributors() {
List<String> contributors = new ArrayList<String>(getAuthorScores().keySet());
Collections.sort(contributors, new DescendingOrder());
return contributors;
}
/**
* List the authors that have contributed but, as opposed to listContributors, also include those
* who have no contribution according to this score.
* These users are counted with a score of 0.
* Will block if scoresReady flag is set to false.
* <p/>
* Contributors are sorted from most points to least.
* If both contributors have the same amount of points then they are sorted alphabetically.
*
* @return a collection with all users sorted by amount of contribution.
*/
public List<String> listContributorsFillWithNoContributors() {
HashSet<String> all = new HashSet<String>(getAuthorScores().keySet());
all.addAll(getOwner().getCollabReview().getAuthorManager().listAuthorNames());
List<String> contributors = new ArrayList<String>(all);
Collections.sort(contributors, new DescendingOrder());
return contributors;
}
/**
* Make sure that results of this metric are ready.
*/
public void ensureReady() {
owner.ensureAllScoresReady();
}
/**
* Register a hook that gets informed whenever the cached scores of this metric are updated.
*
* @param reputationMetricUpdatedHook the hook to inform the cached scores are updated
*/
public void registerReputationMetricUpdatedHook(ReputationMetricUpdatedHook reputationMetricUpdatedHook) {
notificationListManager.addHook(reputationMetricUpdatedHook);
}
public void addHook(Hook hook) {
notificationListManager.addHook(hook);
}
@Override
public void removeHook(Hook hook) {
notificationListManager.removeHook(hook);
}
/**
* Get the sum of all individual scores added up.
*
* @return all authors' scores sum
*/
public Float sum() {
Float sum = 0f;
for (Float score : authorScores.values()) {
assert score != null;
sum += score;
}
return sum;
}
/**
* Sorts results in descending order.
* <p/>
* Serializable is needed by MetricDelta which tries to write the data to a file.
*/
public class DescendingOrder implements Comparator<String>, Serializable {
@Override
public int compare(String o1, String o2) {
Float s1 = getAuthorScore(o1);
if (s1 == null) {
s1 = 0f;
}
Float s2 = getAuthorScore(o2);
if (s2 == null) {
s2 = 0f;
}
if (s1 < s2) {
return +1;
} else if (s1 > s2) {
return -1;
} else {
return o1.compareTo(o2);
}
}
}
}