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