blob: 12f312b2aed00578a76c9da453c6c210356191b9 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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 org.apache.wiki.plugin;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.log4j.Logger;
import org.apache.oro.text.GlobCompiler;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Matcher;
import org.apache.wiki.WikiBackgroundThread;
import org.apache.wiki.WikiContext;
import org.apache.wiki.WikiEngine;
import org.apache.wiki.WikiPage;
import org.apache.wiki.api.exceptions.PluginException;
import org.apache.wiki.api.plugin.InitializablePlugin;
import org.apache.wiki.api.plugin.WikiPlugin;
import org.apache.wiki.event.WikiEngineEvent;
import org.apache.wiki.event.WikiEvent;
import org.apache.wiki.event.WikiEventListener;
import org.apache.wiki.event.WikiPageEvent;
import org.apache.wiki.event.WikiPageRenameEvent;
import org.apache.wiki.references.ReferenceManager;
import org.apache.wiki.util.TextUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
/**
* This plugin counts the number of times a page has been viewed.<br/>
* Parameters:
* <ul>
* <li>count=yes|no</li>
* <li>show=none|count|list</li>
* <li>entries=maximum number of list entries to be returned</li>
* <li>min=minimum page count to be listed</li>
* <li>max=maximum page count to be listed</li>
* <li>sort=name|count</li>
* </ul>
* Default values:<br/>
* <code>show=none sort=name</code>
*
* @since 2.8
*/
public class PageViewPlugin extends AbstractReferralPlugin implements WikiPlugin, InitializablePlugin {
private static final Logger log = Logger.getLogger( PageViewPlugin.class );
/** The page view manager. */
private static PageViewManager c_singleton = null;
/** Constant for the 'count' parameter / value. */
private static final String PARAM_COUNT = "count";
/** Name of the 'entries' parameter. */
private static final String PARAM_MAX_ENTRIES = "entries";
/** Name of the 'max' parameter. */
private static final String PARAM_MAX_COUNT = "max";
/** Name of the 'min' parameter. */
private static final String PARAM_MIN_COUNT = "min";
/** Name of the 'refer' parameter. */
private static final String PARAM_REFER = "refer";
/** Name of the 'sort' parameter. */
private static final String PARAM_SORT = "sort";
/** Constant for the 'none' parameter value. */
private static final String STR_NONE = "none";
/** Constant for the 'list' parameter value. */
private static final String STR_LIST = "list";
/** Constant for the 'yes' parameter value. */
private static final String STR_YES = "yes";
/** Constant for empty string. */
private static final String STR_EMPTY = "";
/** Constant for Wiki markup separator. */
private static final String STR_SEPARATOR = "----";
/** Constant for comma-separated list separator. */
private static final String STR_COMMA = ",";
/** Constant for no-op glob expression. */
private static final String STR_GLOBSTAR = "*";
/** Constant for file storage. */
private static final String COUNTER_PAGE = "PageCount.txt";
/** Constant for storage interval in seconds. */
private static final int STORAGE_INTERVAL = 60;
/**
* Initialize the PageViewPlugin and its singleton.
*
* @param engine The wiki engine.
*/
public void initialize( WikiEngine engine )
{
log.info( "initializing PageViewPlugin" );
synchronized( this )
{
if( c_singleton == null )
{
c_singleton = new PageViewManager( );
}
c_singleton.initialize( engine );
}
}
/**
* Cleanup the singleton reference.
*/
private void cleanup()
{
log.info( "cleaning up PageView Manager" );
c_singleton = null;
}
/**
* {@inheritDoc}
*/
public String execute( WikiContext context, Map<String, String> params ) throws PluginException
{
PageViewManager manager = c_singleton;
String result = STR_EMPTY;
if( manager != null )
{
result = manager.execute( context, params );
}
return result;
}
/**
* Page view manager, handling all storage.
*/
public final class PageViewManager implements WikiEventListener
{
/** Are we initialized? */
private boolean m_initialized = false;
/** The page counters. */
private Map<String, Counter> m_counters = null;
/** The page counters in storage format. */
private Properties m_storage = null;
/** Are all changes stored? */
private boolean m_dirty = false;
/** The page count storage background thread. */
private Thread m_pageCountSaveThread = null;
/** The work directory. */
private String m_workDir = null;
/** Comparator for descending sort on page count. */
private final Comparator<Object> m_compareCountDescending = new Comparator<Object>() {
public int compare( Object o1, Object o2 )
{
final int v1 = getCount( o1 );
final int v2 = getCount( o2 );
return (v1 == v2) ? ((String) o1).compareTo( (String) o2 ) : (v1 < v2) ? 1 : -1;
}
};
/**
* Initialize the page view manager.
*
* @param engine The wiki engine.
*/
public synchronized void initialize( WikiEngine engine )
{
log.info( "initializing PageView Manager" );
m_workDir = engine.getWorkDir();
engine.addWikiEventListener( this );
if( m_counters == null )
{
// Load the counters into a collection
m_storage = new Properties();
m_counters = new TreeMap<String, Counter>();
loadCounters();
}
// backup counters every 5 minutes
if( m_pageCountSaveThread == null )
{
m_pageCountSaveThread = new CounterSaveThread( engine, 5 * STORAGE_INTERVAL, this );
m_pageCountSaveThread.start();
}
m_initialized = true;
}
/**
* Handle the shutdown event via the page counter thread.
*
*/
private synchronized void handleShutdown()
{
log.info( "handleShutdown: The counter store thread was shut down." );
cleanup();
if( m_counters != null )
{
m_dirty = true;
storeCounters();
m_counters.clear();
m_counters = null;
m_storage.clear();
m_storage = null;
}
m_initialized = false;
m_pageCountSaveThread = null;
}
/**
* Inspect wiki events for shutdown.
*
* @param event The wiki event to inspect.
*/
public void actionPerformed( WikiEvent event )
{
if( event instanceof WikiEngineEvent )
{
if( event.getType() == WikiEngineEvent.SHUTDOWN )
{
log.info( "Detected wiki engine shutdown" );
handleShutdown();
}
}
else if( (event instanceof WikiPageRenameEvent) && (event.getType() == WikiPageRenameEvent.PAGE_RENAMED) )
{
String oldPageName = ((WikiPageRenameEvent) event).getOldPageName();
String newPageName = ((WikiPageRenameEvent) event).getNewPageName();
Counter oldCounter = m_counters.get(oldPageName);
if ( oldCounter != null )
{
m_storage.remove(oldPageName);
m_counters.put(newPageName, oldCounter);
m_storage.setProperty(newPageName, oldCounter.toString());
m_counters.remove(oldPageName);
m_dirty = true;
}
}
else if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETED) )
{
String pageName = ((WikiPageEvent) event).getPageName();
m_storage.remove(pageName);
m_counters.remove(pageName);
}
}
/**
* Count a page hit, present a pages' counter or output a list of page counts.
*
* @param context the wiki context
* @param params the plugin parameters
* @return String Wiki page snippet
* @throws PluginException Malformed pattern parameter.
*/
public String execute( WikiContext context, Map<String, String> params ) throws PluginException
{
WikiEngine engine = context.getEngine();
WikiPage page = context.getPage();
String result = STR_EMPTY;
if( page != null )
{
// get parameters
String pagename = page.getName();
String count = params.get( PARAM_COUNT );
String show = params.get( PARAM_SHOW );
int entries = TextUtil.parseIntParameter( params.get( PARAM_MAX_ENTRIES ), Integer.MAX_VALUE );
final int max = TextUtil.parseIntParameter( params.get( PARAM_MAX_COUNT ), Integer.MAX_VALUE );
final int min = TextUtil.parseIntParameter( params.get( PARAM_MIN_COUNT ), Integer.MIN_VALUE );
String sort = params.get( PARAM_SORT );
String body = params.get( DefaultPluginManager.PARAM_BODY );
Pattern[] exclude = compileGlobs( PARAM_EXCLUDE, params.get( PARAM_EXCLUDE ) );
Pattern[] include = compileGlobs( PARAM_INCLUDE, params.get( PARAM_INCLUDE ) );
Pattern[] refer = compileGlobs( PARAM_REFER, params.get( PARAM_REFER ) );
PatternMatcher matcher = (null != exclude || null != include || null != refer) ? new Perl5Matcher() : null;
boolean increment = false;
// increment counter?
if( STR_YES.equals( count ) )
{
increment = true;
}
else
{
count = null;
}
// default increment counter?
if( (show == null || STR_NONE.equals( show )) && count == null )
{
increment = true;
}
// filter on referring pages?
Collection<String> referrers = null;
if( refer != null )
{
ReferenceManager refManager = engine.getReferenceManager();
Iterator< String > iter = refManager.findCreated().iterator();
while ( iter != null && iter.hasNext() )
{
String name = iter.next();
boolean use = false;
for( int n = 0; !use && n < refer.length; n++ )
{
use = matcher.matches( name, refer[n] );
}
if( use )
{
Collection< String > refs = engine.getReferenceManager().findReferrers( name );
if( refs != null && !refs.isEmpty() )
{
if( referrers == null )
{
referrers = new HashSet<String>();
}
referrers.addAll( refs );
}
}
}
}
synchronized( this )
{
Counter counter = m_counters.get( pagename );
// only count in view mode, keep storage values in sync
if( increment && WikiContext.VIEW.equalsIgnoreCase( context.getRequestContext() ) )
{
if( counter == null )
{
counter = new Counter();
m_counters.put( pagename, counter );
}
counter.increment();
m_storage.setProperty( pagename, counter.toString() );
m_dirty = true;
}
if( show == null || STR_NONE.equals( show ) )
{
// nothing to show
}
else if( PARAM_COUNT.equals( show ) )
{
// show page count
if( counter == null )
{
counter = new Counter();
m_counters.put( pagename, counter );
m_storage.setProperty( pagename, counter.toString() );
m_dirty = true;
}
result = counter.toString();
}
else if( body != null && 0 < body.length() && STR_LIST.equals( show ) )
{
// show list of counts
String header = STR_EMPTY;
String line = body;
String footer = STR_EMPTY;
int start = body.indexOf( STR_SEPARATOR );
// split body into header, line, footer on ----
// separator
if( 0 < start )
{
header = body.substring( 0, start );
start = skipWhitespace( start + STR_SEPARATOR.length(), body );
int end = body.indexOf( STR_SEPARATOR, start );
if( start >= end )
{
line = body.substring( start );
}
else
{
line = body.substring( start, end );
end = skipWhitespace( end + STR_SEPARATOR.length(), body );
footer = body.substring( end );
}
}
// sort on name or count?
Map<String, Counter> sorted = m_counters;
if( sort != null && PARAM_COUNT.equals( sort ) )
{
sorted = new TreeMap<String, Counter>( m_compareCountDescending );
sorted.putAll( m_counters );
}
// build a messagebuffer with the list in wiki markup
StringBuffer buf = new StringBuffer( header );
MessageFormat fmt = new MessageFormat( line );
Object[] args = new Object[] { pagename, STR_EMPTY, STR_EMPTY };
Iterator< Entry< String, Counter > > iter = sorted.entrySet().iterator();
while ( iter != null && 0 < entries && iter.hasNext() )
{
Entry< String, Counter > entry = iter.next();
String name = entry.getKey();
// check minimum/maximum count
final int value = entry.getValue().getValue();
boolean use = min <= value && value <= max;
// did we specify a refer-to page?
if( use && referrers != null )
{
use = referrers.contains( name );
}
// did we specify what pages to include?
if( use && include != null )
{
use = false;
for( int n = 0; !use && n < include.length; n++ )
{
use = matcher.matches( name, include[n] );
}
}
// did we specify what pages to exclude?
if( use && null != exclude )
{
for( int n = 0; use && n < exclude.length; n++ )
{
use &= !matcher.matches( name, exclude[n] );
}
}
if( use )
{
args[1] = engine.getRenderingManager().beautifyTitle( name );
args[2] = entry.getValue();
fmt.format( args, buf, null );
entries--;
}
}
buf.append( footer );
// let the engine render the list
result = engine.textToHTML( context, buf.toString() );
}
}
}
return result;
}
/**
* Compile regexp parameter.
*
* @param name The name of the parameter.
* @param value The parameter value.
* @return Pattern[] The compiled patterns, or <code>null</code>.
* @throws PluginException On malformed patterns.
*/
private Pattern[] compileGlobs( String name, String value ) throws PluginException
{
Pattern[] result = null;
if( value != null && 0 < value.length() && !STR_GLOBSTAR.equals( value ) )
{
try
{
PatternCompiler pc = new GlobCompiler();
String[] ptrns = StringUtils.split( value, STR_COMMA );
result = new Pattern[ptrns.length];
for( int n = 0; n < ptrns.length; n++ )
{
result[n] = pc.compile( ptrns[n] );
}
}
catch( MalformedPatternException e )
{
throw new PluginException( "Parameter " + name + " has a malformed pattern: " + e.getMessage() );
}
}
return result;
}
/**
* Adjust offset skipping whitespace.
*
* @param offset The offset in value to adjust.
* @param value String in which offset points.
* @return int Adjusted offset into value.
*/
private int skipWhitespace( int offset, String value )
{
while ( Character.isWhitespace( value.charAt( offset ) ) )
{
offset++;
}
return offset;
}
/**
* Retrieve a page count.
*
* @return int The page count for the given key.
* @param key the key for the Counter
*/
protected int getCount( Object key )
{
return m_counters.get( key ).getValue();
}
/**
* Load the page view counters from file.
*/
private void loadCounters() {
if( m_counters != null && m_storage != null ) {
log.info( "Loading counters." );
synchronized( this ) {
try( InputStream fis = new FileInputStream( new File( m_workDir, COUNTER_PAGE ) ) ) {
m_storage.load( fis );
} catch( IOException ioe ) {
log.error( "Can't load page counter store: " + ioe.getMessage() + " , will create a new one!" );
}
// Copy the collection into a sorted map
Iterator< Entry< Object, Object > > iter = m_storage.entrySet().iterator();
while ( iter != null && iter.hasNext() ) {
Entry< ?, ? > entry = iter.next();
m_counters.put( (String) entry.getKey(), new Counter( (String) entry.getValue() ) );
}
log.info( "Loaded " + m_counters.size() + " counter values." );
}
}
}
/**
* Save the page view counters to file.
*/
protected void storeCounters() {
if( m_counters != null && m_storage != null && m_dirty ) {
log.info( "Storing " + m_counters.size() + " counter values." );
synchronized( this ) {
// Write out the collection of counters
try( final OutputStream fos = new FileOutputStream( new File( m_workDir, COUNTER_PAGE ) ) ) {
m_storage.store( fos, "\n# The number of times each page has been viewed.\n# Do not modify.\n" );
fos.flush();
m_dirty = false;
} catch( IOException ioe ) {
log.error( "Couldn't store counters values: " + ioe.getMessage() );
}
}
}
}
/**
* Is the given thread still current?
*
* @return boolean <code>true</code> if the thread is still the current
* background thread.
* @param thrd
*/
private synchronized boolean isRunning( Thread thrd )
{
return m_initialized && thrd == m_pageCountSaveThread;
}
}
/**
* Counter for page hits collection.
*/
private static final class Counter
{
/** The count value. */
private int m_count = 0;
/**
* Create a new counter.
*/
public Counter()
{
}
/**
* Create and initialize a new counter.
*
* @param value Count value.
*/
public Counter( String value )
{
setValue( value );
}
/**
* Increment counter.
*/
public void increment()
{
m_count++;
}
/**
* Get the count value.
*
* @return int
*/
public int getValue()
{
return m_count;
}
/**
* Set the count value.
*
* @param value String representation of the count.
*/
public void setValue( String value )
{
m_count = NumberUtils.toInt( value );
}
/**
* @return String String representation of the count.
*/
public String toString()
{
return String.valueOf( m_count );
}
}
/**
* Background thread storing the page counters.
*/
static final class CounterSaveThread extends WikiBackgroundThread
{
/** The page view manager. */
private final PageViewManager m_manager;
/**
* Create a wiki background thread to store the page counters.
*
* @param engine The wiki engine.
* @param interval Delay in seconds between saves.
* @param pageViewManager
*/
public CounterSaveThread( WikiEngine engine, int interval, PageViewManager pageViewManager )
{
super( engine, interval );
if( pageViewManager == null )
{
throw new IllegalArgumentException( "Manager cannot be null" );
}
m_manager = pageViewManager;
}
/**
* Save the page counters to file.
*/
public void backgroundTask()
{
if( m_manager.isRunning( this ) )
{
m_manager.storeCounters();
}
}
}
}