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