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}