001 /*
002 Licensed to the Apache Software Foundation (ASF) under one
003 or more contributor license agreements. See the NOTICE file
004 distributed with this work for additional information
005 regarding copyright ownership. The ASF licenses this file
006 to you under the Apache License, Version 2.0 (the
007 "License"); you may not use this file except in compliance
008 with the License. You may obtain a copy of the License at
009
010 http://www.apache.org/licenses/LICENSE-2.0
011
012 Unless required by applicable law or agreed to in writing,
013 software distributed under the License is distributed on an
014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 KIND, either express or implied. See the License for the
016 specific language governing permissions and limitations
017 under the License.
018 */
019 package org.apache.wiki.plugin;
020
021 import java.io.File;
022 import java.io.FileInputStream;
023 import java.io.FileOutputStream;
024 import java.io.IOException;
025 import java.io.InputStream;
026 import java.io.OutputStream;
027 import java.text.MessageFormat;
028 import java.util.Collection;
029 import java.util.Comparator;
030 import java.util.HashSet;
031 import java.util.Iterator;
032 import java.util.Map;
033 import java.util.Map.Entry;
034 import java.util.Properties;
035 import java.util.TreeMap;
036
037 import org.apache.commons.io.IOUtils;
038 import org.apache.commons.lang.StringUtils;
039 import org.apache.commons.lang.math.NumberUtils;
040 import org.apache.log4j.Logger;
041 import org.apache.oro.text.GlobCompiler;
042 import org.apache.oro.text.regex.MalformedPatternException;
043 import org.apache.oro.text.regex.Pattern;
044 import org.apache.oro.text.regex.PatternCompiler;
045 import org.apache.oro.text.regex.PatternMatcher;
046 import org.apache.oro.text.regex.Perl5Matcher;
047 import org.apache.wiki.ReferenceManager;
048 import org.apache.wiki.WikiBackgroundThread;
049 import org.apache.wiki.WikiContext;
050 import org.apache.wiki.WikiEngine;
051 import org.apache.wiki.WikiPage;
052 import org.apache.wiki.api.exceptions.PluginException;
053 import org.apache.wiki.api.plugin.InitializablePlugin;
054 import org.apache.wiki.api.plugin.WikiPlugin;
055 import org.apache.wiki.event.WikiEngineEvent;
056 import org.apache.wiki.event.WikiEvent;
057 import org.apache.wiki.event.WikiEventListener;
058 import org.apache.wiki.event.WikiPageEvent;
059 import org.apache.wiki.event.WikiPageRenameEvent;
060 import org.apache.wiki.util.TextUtil;
061
062
063 /**
064 * This plugin counts the number of times a page has been viewed.<br/>
065 * Parameters:
066 * <ul>
067 * <li>count=yes|no</li>
068 * <li>show=none|count|list</li>
069 * <li>entries=maximum number of list entries to be returned</li>
070 * <li>min=minimum page count to be listed</li>
071 * <li>max=maximum page count to be listed</li>
072 * <li>sort=name|count</li>
073 * </ul>
074 * Default values:<br/>
075 * <code>show=none sort=name</code>
076 *
077 * @since 2.8
078 */
079 public class PageViewPlugin extends AbstractReferralPlugin implements WikiPlugin, InitializablePlugin
080 {
081 private static final Logger log = Logger.getLogger( PageViewPlugin.class );
082
083 /** The page view manager. */
084 private static PageViewManager c_singleton = null;
085
086 /** Constant for the 'count' parameter / value. */
087 private static final String PARAM_COUNT = "count";
088
089 /** Name of the 'entries' parameter. */
090 private static final String PARAM_MAX_ENTRIES = "entries";
091
092 /** Name of the 'max' parameter. */
093 private static final String PARAM_MAX_COUNT = "max";
094
095 /** Name of the 'min' parameter. */
096 private static final String PARAM_MIN_COUNT = "min";
097
098 /** Name of the 'refer' parameter. */
099 private static final String PARAM_REFER = "refer";
100
101 /** Name of the 'sort' parameter. */
102 private static final String PARAM_SORT = "sort";
103
104 /** Constant for the 'none' parameter value. */
105 private static final String STR_NONE = "none";
106
107 /** Constant for the 'list' parameter value. */
108 private static final String STR_LIST = "list";
109
110 /** Constant for the 'yes' parameter value. */
111 private static final String STR_YES = "yes";
112
113 /** Constant for empty string. */
114 private static final String STR_EMPTY = "";
115
116 /** Constant for Wiki markup separator. */
117 private static final String STR_SEPARATOR = "----";
118
119 /** Constant for comma-separated list separator. */
120 private static final String STR_COMMA = ",";
121
122 /** Constant for no-op glob expression. */
123 private static final String STR_GLOBSTAR = "*";
124
125 /** Constant for file storage. */
126 private static final String COUNTER_PAGE = "PageCount.txt";
127
128 /** Constant for storage interval in seconds. */
129 private static final int STORAGE_INTERVAL = 60;
130
131 /**
132 * Initialize the PageViewPlugin and its singleton.
133 *
134 * @param engine The wiki engine.
135 */
136 public void initialize( WikiEngine engine )
137 {
138
139 log.info( "initializing PageViewPlugin" );
140
141 synchronized( this )
142 {
143 if( c_singleton == null )
144 {
145 c_singleton = new PageViewManager( );
146 }
147 c_singleton.initialize( engine );
148 }
149 }
150
151 /**
152 * Cleanup the singleton reference.
153 */
154 private void cleanup()
155 {
156 log.info( "cleaning up PageView Manager" );
157
158 c_singleton = null;
159 }
160
161 /**
162 * {@inheritDoc}
163 */
164 public String execute( WikiContext context, Map<String, String> params ) throws PluginException
165 {
166 PageViewManager manager = c_singleton;
167 String result = STR_EMPTY;
168
169 if( manager != null )
170 {
171 result = manager.execute( context, params );
172 }
173
174 return result;
175 }
176
177 /**
178 * Page view manager, handling all storage.
179 */
180 public final class PageViewManager implements WikiEventListener
181 {
182 /** Are we initialized? */
183 private boolean m_initialized = false;
184
185 /** The page counters. */
186 private Map<String, Counter> m_counters = null;
187
188 /** The page counters in storage format. */
189 private Properties m_storage = null;
190
191 /** Are all changes stored? */
192 private boolean m_dirty = false;
193
194 /** The page count storage background thread. */
195 private Thread m_pageCountSaveThread = null;
196
197 /** The work directory. */
198 private String m_workDir = null;
199
200 /** Comparator for descending sort on page count. */
201 private final Comparator<Object> m_compareCountDescending = new Comparator<Object>() {
202 public int compare( Object o1, Object o2 )
203 {
204 final int v1 = getCount( o1 );
205 final int v2 = getCount( o2 );
206 return (v1 == v2) ? ((String) o1).compareTo( (String) o2 ) : (v1 < v2) ? 1 : -1;
207 }
208 };
209
210 /**
211 * Initialize the page view manager.
212 *
213 * @param engine The wiki engine.
214 */
215 public synchronized void initialize( WikiEngine engine )
216 {
217 log.info( "initializing PageView Manager" );
218
219 m_workDir = engine.getWorkDir();
220
221 engine.addWikiEventListener( this );
222
223 if( m_counters == null )
224 {
225 // Load the counters into a collection
226 m_storage = new Properties();
227 m_counters = new TreeMap<String, Counter>();
228
229 loadCounters();
230 }
231
232 // backup counters every 5 minutes
233 if( m_pageCountSaveThread == null )
234 {
235 m_pageCountSaveThread = new CounterSaveThread( engine, 5 * STORAGE_INTERVAL, this );
236 m_pageCountSaveThread.start();
237 }
238
239 m_initialized = true;
240 }
241
242 /**
243 * Handle the shutdown event via the page counter thread.
244 *
245 */
246 private synchronized void handleShutdown()
247 {
248 log.info( "handleShutdown: The counter store thread was shut down." );
249
250 cleanup();
251
252 if( m_counters != null )
253 {
254
255 m_dirty = true;
256 storeCounters();
257
258 m_counters.clear();
259 m_counters = null;
260
261 m_storage.clear();
262 m_storage = null;
263 }
264
265 m_initialized = false;
266
267 m_pageCountSaveThread = null;
268 }
269
270 /**
271 * Inspect wiki events for shutdown.
272 *
273 * @param event The wiki event to inspect.
274 */
275 public void actionPerformed( WikiEvent event )
276 {
277 if( event instanceof WikiEngineEvent )
278 {
279 if( event.getType() == WikiEngineEvent.SHUTDOWN )
280 {
281 log.info( "Detected wiki engine shutdown" );
282 handleShutdown();
283 }
284 }
285 else if( (event instanceof WikiPageRenameEvent) && (event.getType() == WikiPageRenameEvent.PAGE_RENAMED) )
286 {
287 String oldPageName = ((WikiPageRenameEvent) event).getOldPageName();
288 String newPageName = ((WikiPageRenameEvent) event).getNewPageName();
289 Counter oldCounter = m_counters.get(oldPageName);
290 if ( oldCounter != null )
291 {
292 m_storage.remove(oldPageName);
293 m_counters.put(newPageName, oldCounter);
294 m_storage.setProperty(newPageName, oldCounter.toString());
295 m_counters.remove(oldPageName);
296 m_dirty = true;
297 }
298 }
299 else if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETED) )
300 {
301 String pageName = ((WikiPageEvent) event).getPageName();
302 m_storage.remove(pageName);
303 m_counters.remove(pageName);
304 }
305 }
306
307 /**
308 * Count a page hit, present a pages' counter or output a list of page counts.
309 *
310 * @param context the wiki context
311 * @param params the plugin parameters
312 * @return String Wiki page snippet
313 * @throws PluginException Malformed pattern parameter.
314 */
315 public String execute( WikiContext context, Map<String, String> params ) throws PluginException
316 {
317 WikiEngine engine = context.getEngine();
318 WikiPage page = context.getPage();
319 String result = STR_EMPTY;
320
321 if( page != null )
322 {
323 // get parameters
324 String pagename = page.getName();
325 String count = params.get( PARAM_COUNT );
326 String show = params.get( PARAM_SHOW );
327 int entries = TextUtil.parseIntParameter( params.get( PARAM_MAX_ENTRIES ), Integer.MAX_VALUE );
328 final int max = TextUtil.parseIntParameter( params.get( PARAM_MAX_COUNT ), Integer.MAX_VALUE );
329 final int min = TextUtil.parseIntParameter( params.get( PARAM_MIN_COUNT ), Integer.MIN_VALUE );
330 String sort = params.get( PARAM_SORT );
331 String body = params.get( DefaultPluginManager.PARAM_BODY );
332 Pattern[] exclude = compileGlobs( PARAM_EXCLUDE, params.get( PARAM_EXCLUDE ) );
333 Pattern[] include = compileGlobs( PARAM_INCLUDE, params.get( PARAM_INCLUDE ) );
334 Pattern[] refer = compileGlobs( PARAM_REFER, params.get( PARAM_REFER ) );
335 PatternMatcher matcher = (null != exclude || null != include || null != refer) ? new Perl5Matcher() : null;
336 boolean increment = false;
337
338 // increment counter?
339 if( STR_YES.equals( count ) )
340 {
341 increment = true;
342 }
343 else
344 {
345 count = null;
346 }
347
348 // default increment counter?
349 if( (show == null || STR_NONE.equals( show )) && count == null )
350 {
351 increment = true;
352 }
353
354 // filter on referring pages?
355 Collection<String> referrers = null;
356
357 if( refer != null )
358 {
359 ReferenceManager refManager = engine.getReferenceManager();
360
361 Iterator< String > iter = refManager.findCreated().iterator();
362
363 while ( iter != null && iter.hasNext() )
364 {
365 String name = iter.next();
366 boolean use = false;
367
368 for( int n = 0; !use && n < refer.length; n++ )
369 {
370 use = matcher.matches( name, refer[n] );
371 }
372
373 if( use )
374 {
375 Collection< String > refs = engine.getReferenceManager().findReferrers( name );
376
377 if( refs != null && !refs.isEmpty() )
378 {
379 if( referrers == null )
380 {
381 referrers = new HashSet<String>();
382 }
383 referrers.addAll( refs );
384 }
385 }
386 }
387 }
388
389 synchronized( this )
390 {
391 Counter counter = m_counters.get( pagename );
392
393 // only count in view mode, keep storage values in sync
394 if( increment && WikiContext.VIEW.equalsIgnoreCase( context.getRequestContext() ) )
395 {
396 if( counter == null )
397 {
398 counter = new Counter();
399 m_counters.put( pagename, counter );
400 }
401 counter.increment();
402 m_storage.setProperty( pagename, counter.toString() );
403 m_dirty = true;
404 }
405
406 if( show == null || STR_NONE.equals( show ) )
407 {
408 // nothing to show
409
410 }
411 else if( PARAM_COUNT.equals( show ) )
412 {
413 // show page count
414 if( counter == null )
415 {
416 counter = new Counter();
417 m_counters.put( pagename, counter );
418 m_storage.setProperty( pagename, counter.toString() );
419 m_dirty = true;
420 }
421 result = counter.toString();
422
423 }
424 else if( body != null && 0 < body.length() && STR_LIST.equals( show ) )
425 {
426 // show list of counts
427 String header = STR_EMPTY;
428 String line = body;
429 String footer = STR_EMPTY;
430 int start = body.indexOf( STR_SEPARATOR );
431
432 // split body into header, line, footer on ----
433 // separator
434 if( 0 < start )
435 {
436 header = body.substring( 0, start );
437
438 start = skipWhitespace( start + STR_SEPARATOR.length(), body );
439
440 int end = body.indexOf( STR_SEPARATOR, start );
441
442 if( start >= end )
443 {
444 line = body.substring( start );
445
446 }
447 else
448 {
449 line = body.substring( start, end );
450
451 end = skipWhitespace( end + STR_SEPARATOR.length(), body );
452
453 footer = body.substring( end );
454 }
455 }
456
457 // sort on name or count?
458 Map<String, Counter> sorted = m_counters;
459
460 if( sort != null && PARAM_COUNT.equals( sort ) )
461 {
462 sorted = new TreeMap<String, Counter>( m_compareCountDescending );
463
464 sorted.putAll( m_counters );
465 }
466
467 // build a messagebuffer with the list in wiki markup
468 StringBuffer buf = new StringBuffer( header );
469 MessageFormat fmt = new MessageFormat( line );
470 Object[] args = new Object[] { pagename, STR_EMPTY, STR_EMPTY };
471 Iterator< Entry< String, Counter > > iter = sorted.entrySet().iterator();
472
473 while ( iter != null && 0 < entries && iter.hasNext() )
474 {
475 Entry< String, Counter > entry = iter.next();
476 String name = entry.getKey();
477
478 // check minimum/maximum count
479 final int value = entry.getValue().getValue();
480 boolean use = min <= value && value <= max;
481
482 // did we specify a refer-to page?
483 if( use && referrers != null )
484 {
485 use = referrers.contains( name );
486 }
487
488 // did we specify what pages to include?
489 if( use && include != null )
490 {
491 use = false;
492
493 for( int n = 0; !use && n < include.length; n++ )
494 {
495 use = matcher.matches( name, include[n] );
496 }
497 }
498
499 // did we specify what pages to exclude?
500 if( use && null != exclude )
501 {
502 for( int n = 0; use && n < exclude.length; n++ )
503 {
504 use &= !matcher.matches( name, exclude[n] );
505 }
506 }
507
508 if( use )
509 {
510 args[1] = engine.beautifyTitle( name );
511 args[2] = entry.getValue();
512
513 fmt.format( args, buf, null );
514
515 entries--;
516 }
517 }
518 buf.append( footer );
519
520 // let the engine render the list
521 result = engine.textToHTML( context, buf.toString() );
522 }
523 }
524 }
525 return result;
526 }
527
528 /**
529 * Compile regexp parameter.
530 *
531 * @param name The name of the parameter.
532 * @param value The parameter value.
533 * @return Pattern[] The compiled patterns, or <code>null</code>.
534 * @throws PluginException On malformed patterns.
535 */
536 private Pattern[] compileGlobs( String name, String value ) throws PluginException
537 {
538 Pattern[] result = null;
539
540 if( value != null && 0 < value.length() && !STR_GLOBSTAR.equals( value ) )
541 {
542 try
543 {
544 PatternCompiler pc = new GlobCompiler();
545
546 String[] ptrns = StringUtils.split( value, STR_COMMA );
547
548 result = new Pattern[ptrns.length];
549
550 for( int n = 0; n < ptrns.length; n++ )
551 {
552 result[n] = pc.compile( ptrns[n] );
553 }
554 }
555 catch( MalformedPatternException e )
556 {
557 throw new PluginException( "Parameter " + name + " has a malformed pattern: " + e.getMessage() );
558 }
559 }
560
561 return result;
562 }
563
564 /**
565 * Adjust offset skipping whitespace.
566 *
567 * @param offset The offset in value to adjust.
568 * @param value String in which offset points.
569 * @return int Adjusted offset into value.
570 */
571 private int skipWhitespace( int offset, String value )
572 {
573 while ( Character.isWhitespace( value.charAt( offset ) ) )
574 {
575 offset++;
576 }
577 return offset;
578 }
579
580 /**
581 * Retrieve a page count.
582 *
583 * @return int The page count for the given key.
584 * @param key the key for the Counter
585 */
586 protected int getCount( Object key )
587 {
588 return m_counters.get( key ).getValue();
589 }
590
591 /**
592 * Load the page view counters from file.
593 */
594 private void loadCounters()
595 {
596 if( m_counters != null && m_storage != null )
597 {
598 log.info( "Loading counters." );
599 synchronized( this )
600 {
601 InputStream fis = null;
602 try
603 {
604 fis = new FileInputStream( new File( m_workDir, COUNTER_PAGE ) );
605 m_storage.load( fis );
606 }
607 catch( IOException ioe )
608 {
609 log.error( "Can't load page counter store: " + ioe.getMessage() + " , will create a new one!" );
610 }
611 finally
612 {
613 IOUtils.closeQuietly( fis );
614 }
615
616 // Copy the collection into a sorted map
617 Iterator< Entry< Object, Object > > iter = m_storage.entrySet().iterator();
618
619 while ( iter != null && iter.hasNext() )
620 {
621 Entry< ?, ? > entry = iter.next();
622
623 m_counters.put( (String) entry.getKey(), new Counter( (String) entry.getValue() ) );
624 }
625
626 log.info( "Loaded " + m_counters.size() + " counter values." );
627 }
628 }
629 }
630
631 /**
632 * Save the page view counters to file.
633 *
634 */
635 protected void storeCounters()
636 {
637 if( m_counters != null && m_storage != null && m_dirty )
638 {
639 log.info( "Storing " + m_counters.size() + " counter values." );
640
641 synchronized( this )
642 {
643 OutputStream fos = null;
644
645 // Write out the collection of counters
646 try
647 {
648 fos = new FileOutputStream( new File( m_workDir, COUNTER_PAGE ) );
649
650 m_storage.store( fos, "\n# The number of times each page has been viewed.\n# Do not modify.\n" );
651 fos.flush();
652
653 m_dirty = false;
654 }
655 catch( IOException ioe )
656 {
657 log.error( "Couldn't store counters values: " + ioe.getMessage() );
658 }
659 finally
660 {
661 IOUtils.closeQuietly( fos );
662 }
663 }
664 }
665 }
666
667 /**
668 * Is the given thread still current?
669 *
670 * @return boolean <code>true</code> if the thread is still the current
671 * background thread.
672 * @param thrd
673 */
674 private synchronized boolean isRunning( Thread thrd )
675 {
676 return m_initialized && thrd == m_pageCountSaveThread;
677 }
678
679 }
680
681
682 /**
683 * Counter for page hits collection.
684 */
685 private static final class Counter
686 {
687
688 /** The count value. */
689 private int m_count = 0;
690
691 /**
692 * Create a new counter.
693 */
694 public Counter()
695 {
696 }
697
698 /**
699 * Create and initialize a new counter.
700 *
701 * @param value Count value.
702 */
703 public Counter( String value )
704 {
705 setValue( value );
706 }
707
708 /**
709 * Increment counter.
710 */
711 public void increment()
712 {
713 m_count++;
714 }
715
716 /**
717 * Get the count value.
718 *
719 * @return int
720 */
721 public int getValue()
722 {
723 return m_count;
724 }
725
726 /**
727 * Set the count value.
728 *
729 * @param value String representation of the count.
730 */
731 public void setValue( String value )
732 {
733 m_count = NumberUtils.toInt( value );
734 }
735
736 /**
737 * @return String String representation of the count.
738 */
739 public String toString()
740 {
741 return String.valueOf( m_count );
742 }
743 }
744
745 /**
746 * Background thread storing the page counters.
747 */
748 static final class CounterSaveThread extends WikiBackgroundThread
749 {
750
751 /** The page view manager. */
752 private final PageViewManager m_manager;
753
754 /**
755 * Create a wiki background thread to store the page counters.
756 *
757 * @param engine The wiki engine.
758 * @param interval Delay in seconds between saves.
759 * @param pageViewManager
760 */
761 public CounterSaveThread( WikiEngine engine, int interval, PageViewManager pageViewManager )
762 {
763
764 super( engine, interval );
765
766 if( pageViewManager == null )
767 {
768 throw new IllegalArgumentException( "Manager cannot be null" );
769 }
770
771 m_manager = pageViewManager;
772 }
773
774 /**
775 * Save the page counters to file.
776 */
777 public void backgroundTask()
778 {
779
780 if( m_manager.isRunning( this ) )
781 {
782 m_manager.storeCounters();
783 }
784 }
785 }
786 }