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}