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 }