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 */ 019package org.apache.wiki.plugin; 020 021import org.apache.commons.lang3.StringUtils; 022import org.apache.logging.log4j.LogManager; 023import org.apache.logging.log4j.Logger; 024import org.apache.wiki.WikiContext; 025import org.apache.wiki.api.core.Context; 026import org.apache.wiki.api.core.ContextEnum; 027import org.apache.wiki.api.core.Engine; 028import org.apache.wiki.api.core.Page; 029import org.apache.wiki.api.exceptions.PluginException; 030import org.apache.wiki.api.plugin.Plugin; 031import org.apache.wiki.attachment.Attachment; 032import org.apache.wiki.i18n.InternationalizationManager; 033import org.apache.wiki.pages.PageManager; 034import org.apache.wiki.preferences.Preferences; 035import org.apache.wiki.preferences.Preferences.TimeFormat; 036import org.apache.wiki.render.RenderingManager; 037import org.apache.wiki.util.TextUtil; 038import org.apache.wiki.util.XHTML; 039import org.apache.wiki.util.XhtmlUtil; 040import org.jdom2.Element; 041 042import java.text.DateFormat; 043import java.text.SimpleDateFormat; 044import java.util.Calendar; 045import java.util.Collection; 046import java.util.Date; 047import java.util.GregorianCalendar; 048import java.util.Locale; 049import java.util.Map; 050import java.util.ResourceBundle; 051 052 053/** 054 * Returns the Recent Changes in the wiki being a date-sorted list of page names. 055 * 056 * <p>Parameters: </p> 057 * <ul> 058 * <li><b>since</b> - show changes from the last n days, for example since=5 shows only the pages that were changed in the last five days</li> 059 * <li><b>format</b> - (full|compact) : if "full", then display a long version with all possible info. If "compact", then be as compact as possible.</li> 060 * <li><b>timeFormat</b> - the time format to use, the default is "HH:mm:ss"</li> 061 * <li><b>dateFormat</b> - the date format to use, the default is "dd.MM.yyyy"</li> 062 * </ul> 063 */ 064public class RecentChangesPlugin extends AbstractReferralPlugin implements Plugin { 065 066 private static final Logger LOG = LogManager.getLogger( RecentChangesPlugin.class ); 067 068 /** Parameter name for the separator format. Value is <tt>{@value}</tt>. */ 069 public static final String PARAM_FORMAT = "format"; 070 /** Parameter name for the separator timeFormat. Value is <tt>{@value}</tt>. */ 071 public static final String PARAM_TIME_FORMAT = "timeFormat"; 072 /** Parameter name for the separator dateFormat. Value is <tt>{@value}</tt>. */ 073 public static final String PARAM_DATE_FORMAT = "dateFormat"; 074 075 /** How many days we show by default. */ 076 private static final int DEFAULT_DAYS = 100*365; 077 public static final String DEFAULT_TIME_FORMAT ="HH:mm:ss"; 078 public static final String DEFAULT_DATE_FORMAT ="dd.MM.yyyy"; 079 080 081 @Override 082 public String getDisplayName(Locale locale) { 083 final ResourceBundle rb = ResourceBundle.getBundle(PluginManager.PLUGIN_I18N_RESOURCE, locale); 084 return rb.getString(this.getClass().getSimpleName()); 085 } 086 087 088 /** 089 * {@inheritDoc} 090 */ 091 @Override 092 public String execute( final Context context, final Map< String, String > params ) throws PluginException { 093 final int since = TextUtil.parseIntParameter( params.get( "since" ), DEFAULT_DAYS ); 094 String spacing = "4"; 095 boolean showAuthor = true; 096 boolean showChangenote = true; 097 String tablewidth = "4"; 098 099 final Engine engine = context.getEngine(); 100 101 // 102 // Which format we want to see? 103 // 104 if( "compact".equals( params.get( PARAM_FORMAT ) ) ) { 105 spacing = "0"; 106 showAuthor = false; 107 showChangenote = false; 108 tablewidth = "2"; 109 } 110 111 final Calendar sincedate = new GregorianCalendar(); 112 sincedate.add( Calendar.DAY_OF_MONTH, -since ); 113 114 LOG.debug("Calculating recent changes from "+sincedate.getTime()); 115 116 // FIXME: Should really have a since date on the getRecentChanges method. 117 Collection< Page > changes = engine.getManager( PageManager.class ).getRecentChanges(); 118 super.initialize( context, params ); 119 changes = filterWikiPageCollection( changes ); 120 121 if ( changes != null ) { 122 Date olddate = new Date( 0 ); 123 124 final DateFormat fmt = getDateFormat( context, params ); 125 final DateFormat tfmt = getTimeFormat( context, params ); 126 127 final Element rt = XhtmlUtil.element( XHTML.table ); 128 rt.setAttribute( XHTML.ATTR_class, "recentchanges" ); 129 rt.setAttribute( XHTML.ATTR_cellpadding, spacing ); 130 131 for( final Page pageref : changes ) { 132 final Date lastmod = pageref.getLastModified(); 133 134 if( lastmod.before( sincedate.getTime() ) ) { 135 break; 136 } 137 138 if( !isSameDay( lastmod, olddate ) ) { 139 final Element row = XhtmlUtil.element( XHTML.tr ); 140 final Element col = XhtmlUtil.element( XHTML.td ); 141 col.setAttribute( XHTML.ATTR_colspan, tablewidth ); 142 col.setAttribute( XHTML.ATTR_class, "date" ); 143 col.addContent( XhtmlUtil.element( XHTML.b, fmt.format( lastmod ) ) ); 144 145 rt.addContent( row ); 146 row.addContent( col ); 147 olddate = lastmod; 148 } 149 150 final String href = context.getURL( pageref instanceof Attachment ? ContextEnum.PAGE_ATTACH.getRequestContext() 151 : ContextEnum.PAGE_VIEW.getRequestContext(), pageref.getName() ); 152 Element link = XhtmlUtil.link( href, engine.getManager( RenderingManager.class ).beautifyTitle( pageref.getName() ) ); 153 final Element row = XhtmlUtil.element( XHTML.tr ); 154 final Element col = XhtmlUtil.element( XHTML.td ); 155 col.setAttribute( XHTML.ATTR_width, "30%" ); 156 col.addContent( link ); 157 158 // 159 // Add the direct link to the attachment info. 160 // 161 if( pageref instanceof Attachment ) { 162 link = XhtmlUtil.link( context.getURL( WikiContext.INFO, pageref.getName() ), null ); 163 link.setAttribute( XHTML.ATTR_class, "infolink" ); 164 165 final Element img = XhtmlUtil.img( context.getURL( WikiContext.NONE, "images/attachment_small.png" ), null ); 166 link.addContent( img ); 167 168 col.addContent( link ); 169 } 170 171 row.addContent( col ); 172 rt.addContent( row ); 173 174 if( pageref instanceof Attachment ) { 175 final Element td = XhtmlUtil.element( XHTML.td, tfmt.format( lastmod ) ); 176 td.setAttribute( XHTML.ATTR_class, "lastchange" ); 177 row.addContent( td ); 178 } else { 179 final Element infocol = XhtmlUtil.element( XHTML.td ); 180 infocol.setAttribute( XHTML.ATTR_class, "lastchange" ); 181 infocol.addContent( XhtmlUtil.link( context.getURL( WikiContext.DIFF, pageref.getName(), "r1=-1" ), 182 tfmt.format( lastmod ) ) ); 183 row.addContent( infocol ); 184 } 185 186 // 187 // Display author information. 188 // 189 if( showAuthor ) { 190 final String author = pageref.getAuthor(); 191 192 final Element authorinfo = XhtmlUtil.element( XHTML.td ); 193 authorinfo.setAttribute( XHTML.ATTR_class, "author" ); 194 195 if( author != null ) { 196 if( engine.getManager( PageManager.class ).wikiPageExists( author ) ) { 197 authorinfo.addContent( XhtmlUtil.link( context.getURL( WikiContext.VIEW, author ), author ) ); 198 } else { 199 authorinfo.addContent( author ); 200 } 201 } else { 202 authorinfo.addContent( Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE ) 203 .getString( "common.unknownauthor" ) ); 204 } 205 206 row.addContent( authorinfo ); 207 } 208 209 // Change note 210 if( showChangenote ) { 211 final String changenote = pageref.getAttribute( Page.CHANGENOTE ); 212 final Element td_changenote = XhtmlUtil.element( XHTML.td, changenote ); 213 td_changenote.setAttribute( XHTML.ATTR_class, "changenote" ); 214 row.addContent( td_changenote ); 215 } 216 217 // Revert note 218/* 219 if( context.hasAdminPermissions() ) 220 { 221 row.addElement( new td("Revert") ); 222 } 223 */ 224 } 225 return XhtmlUtil.serialize( rt, XhtmlUtil.EXPAND_EMPTY_NODES ); 226 } 227 return ""; 228 } 229 230 231 private boolean isSameDay( final Date a, final Date b ) { 232 final Calendar aa = Calendar.getInstance(); aa.setTime( a ); 233 final Calendar bb = Calendar.getInstance(); bb.setTime( b ); 234 235 return aa.get( Calendar.YEAR ) == bb.get( Calendar.YEAR ) 236 && aa.get( Calendar.DAY_OF_YEAR ) == bb.get( Calendar.DAY_OF_YEAR ); 237 } 238 239 240 // TODO: Ideally the default behavior should be to return the default format for the default 241 // locale, but that is at odds with the 1st version of this plugin. We seek to preserve the 242 // behaviour of that first version, so to get the default format, the user must explicitly do 243 // something like: dateFormat='' timeformat='' which is a odd, but probably okay. 244 private DateFormat getTimeFormat( final Context context, final Map< String, String > params ) { 245 final String formatString = get( params, DEFAULT_TIME_FORMAT, PARAM_TIME_FORMAT ); 246 if( StringUtils.isBlank( formatString ) ) { 247 return Preferences.getDateFormat( context, TimeFormat.TIME ); 248 } 249 250 return new SimpleDateFormat( formatString ); 251 } 252 253 private DateFormat getDateFormat( final Context context, final Map< String, String > params ) { 254 final String formatString = get( params, DEFAULT_DATE_FORMAT, PARAM_DATE_FORMAT ); 255 if( StringUtils.isBlank( formatString ) ) { 256 return Preferences.getDateFormat( context, TimeFormat.DATE ); 257 } 258 259 return new SimpleDateFormat( formatString ); 260 } 261 262 private String get( final Map< String, String > params, final String defaultValue, final String paramName ) { 263 final String value = params.get( paramName ); 264 return value == null ? defaultValue : value; 265 } 266 267}