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