blob: 397e6ce4139115433076e2afb020377b2639b5ed [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.wiki.plugin;
import org.apache.log4j.Logger;
import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.core.ContextEnum;
import org.apache.wiki.api.core.Engine;
import org.apache.wiki.api.core.Page;
import org.apache.wiki.api.exceptions.PluginException;
import org.apache.wiki.api.exceptions.ProviderException;
import org.apache.wiki.api.plugin.ParserStagePlugin;
import org.apache.wiki.api.plugin.Plugin;
import org.apache.wiki.api.plugin.PluginElement;
import org.apache.wiki.api.providers.WikiProvider;
import org.apache.wiki.auth.AuthorizationManager;
import org.apache.wiki.auth.permissions.PagePermission;
import org.apache.wiki.pages.PageManager;
import org.apache.wiki.preferences.Preferences;
import org.apache.wiki.preferences.Preferences.TimeFormat;
import org.apache.wiki.references.ReferenceManager;
import org.apache.wiki.render.RenderingManager;
import org.apache.wiki.util.TextUtil;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Builds a simple weblog.
* The pageformat can use the following params:</p>
* <p>%p - Page name</p>
* <p>Parameters:</p>
* <ul>
* <li><b>page</b> - which page is used to do the blog; default is the current page.</li>
* <li><b>entryFormat</b> - how to display the date on pages, using the J2SE SimpleDateFormat
* syntax. Defaults to the current locale's DateFormat.LONG format
* for the date, and current locale's DateFormat.SHORT for the time.
* Thus, for the US locale this will print dates similar to
* this: September 4, 2005 11:54 PM</li>
* <li><b>days</b> - how many days the weblog aggregator should show. If set to
* "all", shows all pages.</li>
* <li><b>pageformat</b> - What the entry pages should look like.</li>
* <li><b>startDate</b> - Date when to start. Format is "ddMMyy."</li>
* <li><b>maxEntries</b> - How many entries to show at most.</li>
* <li><b>preview</b> - How many characters of the text to show on the preview page.</li>
* </ul>
* <p>The "days" and "startDate" can also be sent in HTTP parameters,
* and the names are "weblog.days" and "weblog.startDate", respectively.</p>
* <p>The weblog plugin also adds an attribute to each page it is on:
* "weblogplugin.isweblog" is set to "true". This can be used to quickly
* peruse pages which have weblogs.</p>
* @since 1.9.21
*/
// FIXME: Add "entries" param as an alternative to "days".
// FIXME: Entries arrive in wrong order.
public class WeblogPlugin implements Plugin, ParserStagePlugin {
private static final Logger log = Logger.getLogger(WeblogPlugin.class);
private static final Pattern HEADINGPATTERN;
/** How many days are considered by default. Default value is {@value} */
private static final int DEFAULT_DAYS = 7;
private static final String DEFAULT_PAGEFORMAT = "%p_blogentry_";
/** The default date format used in the blog entry page names. */
public static final String DEFAULT_DATEFORMAT = "ddMMyy";
/** Parameter name for the startDate. Value is <tt>{@value}</tt>. */
public static final String PARAM_STARTDATE = "startDate";
/** Parameter name for the entryFormat. Value is <tt>{@value}</tt>. */
public static final String PARAM_ENTRYFORMAT = "entryFormat";
/** Parameter name for the days. Value is <tt>{@value}</tt>. */
public static final String PARAM_DAYS = "days";
/** Parameter name for the allowComments. Value is <tt>{@value}</tt>. */
public static final String PARAM_ALLOWCOMMENTS = "allowComments";
/** Parameter name for the maxEntries. Value is <tt>{@value}</tt>. */
public static final String PARAM_MAXENTRIES = "maxEntries";
/** Parameter name for the page. Value is <tt>{@value}</tt>. */
public static final String PARAM_PAGE = "page";
/** Parameter name for the preview. Value is <tt>{@value}</tt>. */
public static final String PARAM_PREVIEW = "preview";
/** The attribute which is stashed to the WikiPage attributes to check if a page
* is a weblog or not. You may check for its presence.
*/
public static final String ATTR_ISWEBLOG = "weblogplugin.isweblog";
static {
// This is a pretty ugly, brute-force regex. But it will do for now...
HEADINGPATTERN = Pattern.compile("(<h[1-4][^>]*>)(.*)(</h[1-4]>)", Pattern.CASE_INSENSITIVE);
}
/**
* Create an entry name based on the blogname, a date, and an entry number.
*
* @param pageName Name of the blog
* @param date The date (in ddMMyy format)
* @param entryNum The entry number.
* @return A formatted page name.
*/
public static String makeEntryPage( final String pageName, final String date, final String entryNum ) {
return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date+"_"+entryNum;
}
/**
* Return just the basename for entires without date and entry numebr.
*
* @param pageName The name of the blog.
* @return A formatted name.
*/
public static String makeEntryPage( final String pageName )
{
return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName);
}
/**
* Returns the entry page without the entry number.
*
* @param pageName Blog name.
* @param date The date.
* @return A base name for the blog entries.
*/
public static String makeEntryPage( final String pageName, final String date ) {
return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date;
}
/**
* {@inheritDoc}
*/
@Override
public String execute( final Context context, final Map< String, String > params ) throws PluginException {
final Calendar startTime;
final Calendar stopTime;
int numDays = DEFAULT_DAYS;
final Engine engine = context.getEngine();
final AuthorizationManager mgr = engine.getManager( AuthorizationManager.class );
//
// Parse parameters.
//
String days;
final DateFormat entryFormat;
String startDay;
boolean hasComments = false;
int maxEntries;
String weblogName;
if( (weblogName = params.get(PARAM_PAGE)) == null ) {
weblogName = context.getPage().getName();
}
if( (days = context.getHttpParameter( "weblog."+PARAM_DAYS )) == null ) {
days = params.get( PARAM_DAYS );
}
if( ( params.get(PARAM_ENTRYFORMAT)) == null ) {
entryFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME );
} else {
entryFormat = new SimpleDateFormat( params.get(PARAM_ENTRYFORMAT) );
}
if( days != null ) {
if( days.equalsIgnoreCase("all") ) {
numDays = Integer.MAX_VALUE;
} else {
numDays = TextUtil.parseIntParameter( days, DEFAULT_DAYS );
}
}
if( (startDay = params.get(PARAM_STARTDATE)) == null ) {
startDay = context.getHttpParameter( "weblog."+PARAM_STARTDATE );
}
if( TextUtil.isPositive( params.get(PARAM_ALLOWCOMMENTS) ) ) {
hasComments = true;
}
maxEntries = TextUtil.parseIntParameter( params.get(PARAM_MAXENTRIES), Integer.MAX_VALUE );
//
// Determine the date range which to include.
//
startTime = Calendar.getInstance();
stopTime = Calendar.getInstance();
if( startDay != null ) {
final SimpleDateFormat fmt = new SimpleDateFormat( DEFAULT_DATEFORMAT );
try {
final Date d = fmt.parse( startDay );
startTime.setTime( d );
stopTime.setTime( d );
} catch( final ParseException e ) {
return "Illegal time format: "+startDay;
}
}
//
// Mark this to be a weblog
//
context.getPage().setAttribute(ATTR_ISWEBLOG, "true");
//
// We make a wild guess here that nobody can do millisecond accuracy here.
//
startTime.add( Calendar.DAY_OF_MONTH, -numDays );
startTime.set( Calendar.HOUR, 0 );
startTime.set( Calendar.MINUTE, 0 );
startTime.set( Calendar.SECOND, 0 );
stopTime.set( Calendar.HOUR, 23 );
stopTime.set( Calendar.MINUTE, 59 );
stopTime.set( Calendar.SECOND, 59 );
final StringBuilder sb = new StringBuilder();
final List< Page > blogEntries = findBlogEntries( engine, weblogName, startTime.getTime(), stopTime.getTime() );
blogEntries.sort( new PageDateComparator() );
sb.append("<div class=\"weblog\">\n");
for( final Iterator< Page > i = blogEntries.iterator(); i.hasNext() && maxEntries-- > 0 ; ) {
final Page p = i.next();
if( mgr.checkPermission( context.getWikiSession(), new PagePermission(p, PagePermission.VIEW_ACTION) ) ) {
addEntryHTML( context, entryFormat, hasComments, sb, p, params );
}
}
sb.append("</div>\n");
return sb.toString();
}
/**
* Generates HTML for an entry.
*
* @param context
* @param entryFormat
* @param hasComments True, if comments are enabled.
* @param buffer The buffer to which we add.
* @param entry
* @throws ProviderException
*/
private void addEntryHTML( final Context context, final DateFormat entryFormat, final boolean hasComments,
final StringBuilder buffer, final Page entry, final Map< String, String > params) {
final Engine engine = context.getEngine();
final ResourceBundle rb = Preferences.getBundle(context, Plugin.CORE_PLUGINS_RESOURCEBUNDLE);
buffer.append("<div class=\"weblogentry\">\n");
//
// Heading
//
buffer.append("<div class=\"weblogentryheading\">\n");
final Date entryDate = entry.getLastModified();
buffer.append( entryFormat != null ? entryFormat.format(entryDate) : entryDate );
buffer.append("</div>\n");
//
// Append the text of the latest version. Reset the context to that page.
//
final Context entryCtx = context.clone();
entryCtx.setPage( entry );
String html = engine.getManager( RenderingManager.class ).getHTML( entryCtx, engine.getManager( PageManager.class ).getPage( entry.getName() ) );
// Extract the first h1/h2/h3 as title, and replace with null
buffer.append("<div class=\"weblogentrytitle\">\n");
final Matcher matcher = HEADINGPATTERN.matcher( html );
if ( matcher.find() ) {
final String title = matcher.group(2);
html = matcher.replaceFirst("");
buffer.append( title );
} else {
buffer.append( entry.getName() );
}
buffer.append("</div>\n");
buffer.append("<div class=\"weblogentrybody\">\n");
final int preview = TextUtil.parseIntParameter(params.get(PARAM_PREVIEW), 0);
if (preview > 0) {
//
// We start with the first 'preview' number of characters from the text,
// and then add characters to it until we get to a linebreak or a period.
// The idea is that cutting off at a linebreak is less likely
// to disturb the HTML and leave us with garbled output.
//
boolean hasBeenCutOff = false;
int cutoff = Math.min(preview, html.length());
while (cutoff < html.length()) {
if (html.charAt(cutoff) == '\r' || html.charAt(cutoff) == '\n') {
hasBeenCutOff = true;
break;
} else if (html.charAt(cutoff) == '.') {
// we do want the period
cutoff++;
hasBeenCutOff = true;
break;
}
cutoff++;
}
buffer.append(html.substring(0, cutoff));
if (hasBeenCutOff) {
buffer.append(" <a href=\""+entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), entry.getName())+"\">"+rb.getString("weblogentryplugin.more")+"</a>\n");
}
} else {
buffer.append(html);
}
buffer.append("</div>\n");
//
// Append footer
//
buffer.append("<div class=\"weblogentryfooter\">\n");
String author = entry.getAuthor();
if( author != null ) {
if( engine.getManager( PageManager.class ).wikiPageExists(author) ) {
author = "<a href=\""+entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), author )+"\">"+engine.getManager( RenderingManager.class ).beautifyTitle(author)+"</a>";
}
} else {
author = "AnonymousCoward";
}
buffer.append( MessageFormat.format( rb.getString("weblogentryplugin.postedby"), author));
buffer.append( "<a href=\"" + entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), entry.getName() ) + "\">" + rb.getString("weblogentryplugin.permalink") + "</a>" );
final String commentPageName = TextUtil.replaceString( entry.getName(), "blogentry", "comments" );
if( hasComments ) {
final int numComments = guessNumberOfComments( engine, commentPageName );
//
// We add the number of comments to the URL so that the user's browsers would realize that the page has changed.
//
buffer.append( "&nbsp;&nbsp;" );
final String addcomment = rb.getString("weblogentryplugin.addcomment");
buffer.append( "<a href=\""+
entryCtx.getURL( ContextEnum.PAGE_COMMENT.getRequestContext(), commentPageName, "nc=" + numComments ) + "\">" +
MessageFormat.format( addcomment, numComments ) +
"</a>" );
}
buffer.append("</div>\n");
// Done, close
buffer.append("</div>\n");
}
private int guessNumberOfComments( final Engine engine, final String commentpage ) {
final String pagedata = engine.getManager( PageManager.class ).getPureText( commentpage, WikiProvider.LATEST_VERSION );
if( pagedata == null || pagedata.trim().length() == 0 ) {
return 0;
}
return TextUtil.countSections( pagedata );
}
/**
* Attempts to locate all pages that correspond to the
* blog entry pattern. Will only consider the days on the dates; not the hours and minutes.
*
* @param engine Engine which is used to get the pages
* @param baseName The basename (e.g. "Main" if you want "Main_blogentry_xxxx")
* @param start The date which is the first to be considered
* @param end The end date which is the last to be considered
* @return a list of pages with their FIRST revisions.
*/
public List< Page > findBlogEntries( final Engine engine, String baseName, final Date start, final Date end ) {
final PageManager mgr = engine.getManager( PageManager.class );
final Set< String > allPages = engine.getManager( ReferenceManager.class ).findCreated();
final ArrayList< Page > result = new ArrayList<>();
baseName = makeEntryPage( baseName );
for( final String pageName : allPages ) {
if( pageName.startsWith( baseName ) ) {
try {
final Page firstVersion = mgr.getPageInfo( pageName, 1 );
final Date d = firstVersion.getLastModified();
if( d.after( start ) && d.before( end ) ) {
result.add( firstVersion );
}
} catch( final Exception e ) {
log.debug( "Page name :" + pageName + " was suspected as a blog entry but it isn't because of parsing errors", e );
}
}
}
return result;
}
/**
* Reverse comparison.
*/
private static class PageDateComparator implements Comparator< Page > {
/**{@inheritDoc}*/
@Override
public int compare( final Page page1, final Page page2 ) {
if( page1 == null || page2 == null ) {
return 0;
}
return page2.getLastModified().compareTo( page1.getLastModified() );
}
}
/**
* Mark us as being a real weblog.
*
* {@inheritDoc}
*/
@Override
public void executeParser( final PluginElement element, final Context context, final Map< String, String > params ) {
context.getPage().setAttribute( ATTR_ISWEBLOG, "true" );
}
}