/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.stats.impl.xwiki;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReferenceSerializer;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.criteria.impl.Duration;
import com.xpn.xwiki.criteria.impl.Period;
import com.xpn.xwiki.criteria.impl.Range;
import com.xpn.xwiki.criteria.impl.RangeFactory;
import com.xpn.xwiki.criteria.impl.Scope;
import com.xpn.xwiki.stats.impl.DocumentStats;
import com.xpn.xwiki.stats.impl.RefererStats;
import com.xpn.xwiki.stats.impl.StatsUtil;
import com.xpn.xwiki.stats.impl.StatsUtil.PeriodType;
import com.xpn.xwiki.stats.impl.VisitStats;
import com.xpn.xwiki.store.XWikiHibernateStore;
import com.xpn.xwiki.web.DownloadAction;
import com.xpn.xwiki.web.SaveAction;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.web.ViewAction;
/**
* Reader statistics from XWiki database.
*
* @version $Id: 67b9a5d052e36e421e8e99cc262b5a1cb9a885ed $
* @since 1.4M2
*/
public class XWikiStatsReader
{
/**
* Logging tool.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(XWikiStatsReader.class);
/**
* Used to convert a proper Document Reference to a string but without the wiki name.
*/
private EntityReferenceSerializer<String> compactwikiEntityReferenceSerializer = Utils.getComponent(
EntityReferenceSerializer.TYPE_STRING, "compactwiki");
/**
* Return the statistics action stored.
*
* @param action the action.
* @param size the maximum size of the list to return.
* @param context the XWiki context.
* @return the list of recent statistics action stored.
*/
public Collection<Object> getRecentActions(String action, int size, XWikiContext context)
{
List<Object> list = new ArrayList<Object>();
if ((action.equals(ViewAction.VIEW_ACTION) || (action.equals(SaveAction.ACTION_NAME)))) {
Collection<?> actions = StatsUtil.getRecentActionFromSessions(context, action);
if (actions != null) {
Object[] actionsarray = actions.toArray();
CollectionUtils.reverseArray(actionsarray);
int nb = Math.min(actions.size(), size);
for (int i = 0; i < nb; i++) {
list.add(actionsarray[i]);
}
}
}
return list;
}
/**
* @param range the range.
* @return the corresponding sort order.
*/
private String getHqlSortOrderFromRange(Range range)
{
String sortOrder;
if (range.getSize() < 0) {
sortOrder = "asc";
} else {
sortOrder = "desc";
}
return sortOrder;
}
/**
* @param domain the provided domain.
* @return the domain to use in HQL query.
*/
private String getHqlValidDomain(String domain)
{
if (domain == null || domain.trim().length() == 0) {
return "%";
}
return domain;
}
/**
* @param scope the set of documents for which to retrieve statistics.
* @param paramList the values to insert in the SQL query.
* @return the name filter HQL query part.
*/
private String getHqlNameFilterFromScope(Scope scope, List<Object> paramList)
{
String nameFilter;
// Note that these queries are made to work with Oracle which treats empty strings as null.
if (scope.getType() == Scope.SPACE_SCOPE && StringUtils.isEmpty(scope.getName())) {
// Select all names that are not page names (i.e. they are space names), excluding empty names which
// represent global scopes.
nameFilter = "name not like '%.%' and (name <> '' or (name is not null and '' is null))";
} else if (scope.getType() == Scope.GLOBAL_SCOPE && StringUtils.isEmpty(scope.getName())) {
// Select all names that are empty (i.e. global)
nameFilter = "name = '' or name is null";
} else if (scope.getType() == Scope.PAGE_SCOPE && StringUtils.isEmpty(scope.getName())) {
// Select all names that are page names
nameFilter = "name like '%.%'";
} else {
nameFilter = "name like ?";
paramList.add(scope.getPattern());
}
return nameFilter;
}
/**
* Shows how the statistics for the specified action have evolved over the specified period of time.
*
* @param action the action for which to retrieve statistics.
* @param scope the set of documents to consider.
* @param period the period of time, including its start date but excluding its end date.
* @param step the step used for sampling the period.
* @param context the XWiki context.
* @return a map of (date, actionCount) pairs.
*/
public Map<DateTime, Integer> getActionStatistics(String action, Scope scope, Period period, Duration step,
XWikiContext context)
{
DateTime stepStart = new DateTime(period.getStart());
DateTime periodEnd = new DateTime(period.getEnd());
org.joda.time.Period stepDuration =
new org.joda.time.Period(step.getYears(), step.getMonths(), step.getWeeks(), step.getDays(), 0, 0, 0, 0);
Map<DateTime, Integer> activity = new HashMap<DateTime, Integer>();
while (stepStart.compareTo(periodEnd) < 0) {
DateTime stepEnd = stepStart.plus(stepDuration);
if (stepEnd.compareTo(periodEnd) > 0) {
stepEnd = periodEnd;
}
List<DocumentStats> stats =
getDocumentStatistics(action, scope, new Period(stepStart.getMillis(), stepEnd.getMillis()),
RangeFactory.FIRST, context);
int actionCount = 0;
if (stats.size() > 0) {
actionCount = stats.get(0).getPageViews();
}
activity.put(stepStart, actionCount);
stepStart = stepEnd;
}
return activity;
}
/**
* Retrieves document statistics.
*
* @param action the action the results should be ordered by. It can be one of: "view", "save" or "download". If the
* action is "view" then the documents are ordered by the number of times they have been viewed so far.
* @param scope the set of documents for which to retrieve statistics.
* @param period the period of time, including its start date but excluding its end date.
* @param range the sub-range to return from the entire result set. Use this parameter for pagination.
* @param context the XWiki context.
* @return A list of DocumentStats objects
*/
public List<DocumentStats> getDocumentStatistics(String action, Scope scope, Period period, Range range,
XWikiContext context)
{
List<DocumentStats> documentStatsList;
List<Object> paramList = new ArrayList<Object>(4);
String nameFilter = getHqlNameFilterFromScope(scope, paramList);
String sortOrder = getHqlSortOrderFromRange(range);
XWikiHibernateStore store = context.getWiki().getHibernateStore();
try {
String query =
MessageFormat.format("select name, sum(pageViews) from DocumentStats"
+ " where ({0}) and action=? and ? <= period and period < ? group by name order"
+ " by sum(pageViews) {1}", nameFilter, sortOrder);
paramList.add(action);
paramList.add(period.getStartCode());
paramList.add(period.getEndCode());
List<?> solist =
store.search(query, range.getAbsoluteSize(), range.getAbsoluteStart(), paramList, context);
documentStatsList = getDocumentStatistics(solist, action);
if (range.getSize() < 0) {
Collections.reverse(documentStatsList);
}
} catch (XWikiException e) {
documentStatsList = Collections.emptyList();
}
return documentStatsList;
}
/**
* Converts the rows retrieved from the database to a list of DocumentStats instances.
*
* @param resultSet the result of a database query for document statistics.
* @param action the action for which the statistics were retrieved.
* @return a list of {@link com.xpn.xwiki.stats.impl.DocumentStats} objects.
* @see #getDocumentStatistics(String, Scope, com.xpn.xwiki.criteria.impl.Period , Range , XWikiContext)
*/
private List<DocumentStats> getDocumentStatistics(List<?> resultSet, String action)
{
List<DocumentStats> documentStatsList = new ArrayList<DocumentStats>(resultSet.size());
Date now = new Date();
for (Object name : resultSet) {
Object[] result = (Object[]) name;
// We can't represent a custom period (e.g. year, week or some time interval) in the
// database and thus we use a default one, which sould be ignored
DocumentStats docStats = new DocumentStats((String) result[0], action, now, PeriodType.DAY);
docStats.setPageViews(((Number) result[1]).intValue());
documentStatsList.add(docStats);
}
return documentStatsList;
}
/**
* Retrieves back-link statistics.
*
* @param domain the domain used for filtering the results.
* @param scope the scope of referred documents for which to retrieve statistics.
* @param period the period of time, including its start date but excluding its end date.
* @param range the sub-range to return from the entire result set. Use this parameter for pagination.
* @param context the XWiki context.
* @return a list of DocumentStats objects.
*/
public List<DocumentStats> getBackLinkStatistics(String domain, Scope scope, Period period, Range range,
XWikiContext context)
{
List<DocumentStats> documentStatsList;
List<Object> paramList = new ArrayList<Object>(4);
String nameFilter = getHqlNameFilterFromScope(scope, paramList);
String sortOrder = getHqlSortOrderFromRange(range);
XWikiHibernateStore store = context.getWiki().getHibernateStore();
try {
String query =
MessageFormat.format("select name, sum(pageViews) from RefererStats"
+ " where ({0}) and referer like ? and ? <= period and period < ? group by name"
+ " order by sum(pageViews) {1}", nameFilter, sortOrder);
paramList.add(getHqlValidDomain(domain));
paramList.add(period.getStartCode());
paramList.add(period.getEndCode());
List<?> solist =
store.search(query, range.getAbsoluteSize(), range.getAbsoluteStart(), paramList, context);
documentStatsList = getDocumentStatistics(solist, "refer");
if (range.getSize() < 0) {
Collections.reverse(documentStatsList);
}
} catch (XWikiException e) {
documentStatsList = Collections.emptyList();
}
return documentStatsList;
}
/**
* Retrieves referrer statistics.
*
* @param domain the domain for which to retrieve statistics. To retrieve statistics for all domains use the empty
* string.
* @param scope the scope of referred documents to use for filtering the results.
* @param period the period of time, including its start date but excluding its end date.
* @param range the sub-range to return from the entire result set. Use this parameter for pagination.
* @param context the XWiki context.
* @return a list of RefererStats objects.
*/
public List<RefererStats> getRefererStatistics(String domain, Scope scope, Period period, Range range,
XWikiContext context)
{
List<RefererStats> refererList;
List<Object> paramList = new ArrayList<Object>(4);
String nameFilter = getHqlNameFilterFromScope(scope, paramList);
String sortOrder = getHqlSortOrderFromRange(range);
XWikiHibernateStore store = context.getWiki().getHibernateStore();
try {
String query =
MessageFormat.format("select referer, sum(pageViews) from RefererStats"
+ " where ({0}) and referer like ? and ? <= period and period < ?"
+ " group by referer order by sum(pageViews) {1}", nameFilter, sortOrder);
paramList.add(getHqlValidDomain(domain));
paramList.add(period.getStartCode());
paramList.add(period.getEndCode());
List<?> solist =
store.search(query, range.getAbsoluteSize(), range.getAbsoluteStart(), paramList, context);
refererList = getRefererStatistics(solist);
if (range.getSize() < 0) {
Collections.reverse(refererList);
}
} catch (XWikiException e) {
refererList = Collections.emptyList();
}
return refererList;
}
/**
* Converts the rows retrieved from the database to a list of {@link com.xpn.xwiki.stats.impl.RefererStats}
* instances.
*
* @param resultSet The result of a database query for referer statistics
* @return A list of {@link com.xpn.xwiki.stats.impl.RefererStats} objects
* @see #getRefererStatistics(String, Scope, Period, Range , XWikiContext)
*/
private List<RefererStats> getRefererStatistics(List<?> resultSet)
{
Date now = new Date();
List<RefererStats> stats = new ArrayList<RefererStats>(resultSet.size());
for (Object name : resultSet) {
Object[] result = (Object[]) name;
// We can't represent a custom period (e.g. year, week or some time interval) in the
// database and thus we use a default one, which sould be ignored
RefererStats refStats = new RefererStats("", (String) result[0], now, PeriodType.DAY);
refStats.setPageViews(((Number) result[1]).intValue());
stats.add(refStats);
}
return stats;
}
/**
* Generate the HQL query used in {@link #getVisitStatistics(String, Period, Range, XWikiContext)}.
*
* @param action the action the results should be ordered by. It can be one of: "view", "save" or "download". If the
* action is "view" then the visitors are ordered by the number of pages they have viewed so far.
* @param period the period of time, including its start date but excluding its end date.
* @param range the sub-range to return from the entire result set. Use this parameter for pagination.
* @param paramList the list of query parameters to fill.
* @param context the XWiki context.
* @return the HQL query
*/
private String createVisitStatisticsQuery(String action, Period period, Range range, List<Object> paramList,
XWikiContext context)
{
StringBuilder query = new StringBuilder("select name, sum(pageSaves), sum(pageViews), sum(downloads)");
query.append(" from VisitStats");
query.append(" where");
// user filter
StringBuilder userListWhere = new StringBuilder();
try {
for (DocumentReference user : StatsUtil.getRequestFilteredUsers(context)) {
if (userListWhere.length() > 0) {
userListWhere.append(", ");
}
userListWhere.append('?');
paramList.add(this.compactwikiEntityReferenceSerializer.serialize(user));
}
} catch (Exception e) {
LOGGER.error("Faild to get filter users list", e);
}
if (userListWhere.length() > 0) {
query.append(" name NOT IN (");
query.append(userListWhere);
query.append(") and ");
}
// date filter
query.append(" ? <= startDate and endDate < ? group by name");
paramList.add(new Date(period.getStart()));
paramList.add(new Date(period.getEnd()));
// order
query.append(' ');
String sortOrder = getHqlSortOrderFromRange(range);
if (action.equals(SaveAction.ACTION_NAME)) {
query.append("order by sum(pageSaves) " + sortOrder);
} else if (action.equals(ViewAction.VIEW_ACTION)) {
query.append("order by sum(pageViews) " + sortOrder);
} else if (action.equals(DownloadAction.ACTION_NAME)) {
query.append("order by sum(downloads) " + sortOrder);
} else {
query.append(MessageFormat.format("order by sum(pageSaves) {0}, sum(pageViews) {0}, sum(downloads) {0}",
sortOrder));
}
return query.toString();
}
/**
* Retrieves visit statistics.
*
* @param action the action the results should be ordered by. It can be one of: "view", "save" or "download". If the
* action is "view" then the visitors are ordered by the number of pages they have viewed so far.
* @param period the period of time, including its start date but excluding its end date.
* @param range the sub-range to return from the entire result set. Use this parameter for pagination.
* @param context the XWiki context.
* @return a list of VisitStats objects.
*/
public List<VisitStats> getVisitStatistics(String action, Period period, Range range, XWikiContext context)
{
List<VisitStats> visiStatList;
List<Object> paramList = new ArrayList<Object>(2);
String query = createVisitStatisticsQuery(action, period, range, paramList, context);
XWikiHibernateStore store = context.getWiki().getHibernateStore();
try {
List<?> solist =
store.search(query, range.getAbsoluteSize(), range.getAbsoluteStart(), paramList, context);
visiStatList = getVisitStatistics(solist, new DateTime(period.getStart()), new DateTime(period.getEnd()));
if (range.getSize() < 0) {
Collections.reverse(visiStatList);
}
} catch (XWikiException e) {
LOGGER.error("Faild to search for vist statistics", e);
visiStatList = Collections.emptyList();
}
return visiStatList;
}
/**
* Converts the rows retrieved from the database to a list of VisitStats instances.
*
* @param resultSet the result of a database query for visitor statistics.
* @param startDate the start date used in the query.
* @param endDate the end date used in the query.
* @return a list of {@link com.xpn.xwiki.stats.impl.VisitStats} objects.
* @see #getVisitStatistics(String, Period, Range, XWikiContext)
*/
private List<VisitStats> getVisitStatistics(List<?> resultSet, DateTime startDate, DateTime endDate)
{
List<VisitStats> stats = new ArrayList<VisitStats>(resultSet.size());
for (Object name2 : resultSet) {
Object[] result = (Object[]) name2;
String name = (String) result[0];
String uniqueID = "";
String cookie = "";
String ip = "";
String userAgent = "";
int pageSaves = ((Number) result[1]).intValue();
int pageViews = ((Number) result[2]).intValue();
int downloads = ((Number) result[3]).intValue();
VisitStats vs =
new VisitStats(name, uniqueID, cookie, ip, userAgent, new Date(startDate.getMillis()), PeriodType.DAY);
vs.setStartDate(new Date(startDate.getMillis()));
vs.setEndDate(new Date(endDate.getMillis()));
vs.setPageSaves(pageSaves);
vs.setPageViews(pageViews);
vs.setDownloads(downloads);
stats.add(vs);
}
return stats;
}
// ////////////////////////////////////////////////////////////////////////////////////////
// Deprecated methods
// ////////////////////////////////////////////////////////////////////////////////////////
/**
* Gets monthly statistics on a document for a specific action.
*
* @param docname fully qualified document name.
* @param action can be "view", "edit", "save", etc..
* @param month the month.
* @param context the XWiki context.
* @return DocumentStats - statistics object.
* @deprecated use {@link #getDocumentStatistics(String, Scope, Period, Range , XWikiContext)} instead.
*/
@Deprecated
public DocumentStats getDocMonthStats(String docname, String action, Date month, XWikiContext context)
{
XWikiHibernateStore store = context.getWiki().getHibernateStore();
DocumentStats object = new DocumentStats(docname, action, month, PeriodType.MONTH);
try {
// TODO Fix use of deprecated call.
store.loadXWikiCollection(object, context, true);
return object;
} catch (XWikiException e) {
e.printStackTrace();
return new DocumentStats();
}
}
/**
* Gets monthly referer statistics.
*
* @param docName fully qualified document name.
* @param month the month.
* @param context the XWiki context.
* @return the monthly referer statistics.
* @throws XWikiException error when searching for referer statistics.
* @deprecated use {@link #getRefererStatistics(String, Scope, Period, Range, XWikiContext)} instead.
*/
@Deprecated
public List<?> getRefMonthStats(String docName, Date month, XWikiContext context) throws XWikiException
{
XWikiHibernateStore store = context.getWiki().getHibernateStore();
List<?> solist;
if (store != null) {
List<Object> paramList = new ArrayList<Object>(1);
paramList.add(docName);
solist = store.search("from RefererStats as obj where obj.name=?", 0, 0, paramList, context);
} else {
solist = Collections.emptyList();
}
return solist;
}
}