001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.plugin;
021:
022: import java.text.DateFormat;
023: import java.text.ParseException;
024: import java.text.SimpleDateFormat;
025: import java.util.ArrayList;
026: import java.util.Calendar;
027: import java.util.Collection;
028: import java.util.Collections;
029: import java.util.Comparator;
030: import java.util.Date;
031: import java.util.Iterator;
032: import java.util.List;
033: import java.util.Map;
034: import java.util.regex.Matcher;
035: import java.util.regex.Pattern;
036:
037: import org.apache.log4j.Logger;
038:
039: import com.ecyrd.jspwiki.*;
040: import com.ecyrd.jspwiki.auth.AuthorizationManager;
041: import com.ecyrd.jspwiki.auth.permissions.PagePermission;
042: import com.ecyrd.jspwiki.parser.PluginContent;
043: import com.ecyrd.jspwiki.providers.ProviderException;
044:
045: /**
046: * <p>Builds a simple weblog.
047: * The pageformat can use the following params:</p>
048: * <p>%p - Page name</p>
049: * <p>Parameters:</p>
050: * <ul>
051: * <li>page - which page is used to do the blog; default is the current page.</li>
052: * <li>entryFormat - how to display the date on pages, using the J2SE SimpleDateFormat
053: * syntax. Defaults to the current locale's DateFormat.LONG format
054: * for the date, and current locale's DateFormat.SHORT for the time.
055: * Thus, for the US locale this will print dates similar to
056: * this: September 4, 2005 11:54 PM</li>
057: * <li>days - how many days the weblog aggregator should show. If set to
058: * "all", shows all pages.</li>
059: * <li>pageformat - What the entry pages should look like.</li>
060: * <li>startDate - Date when to start. Format is "ddMMyy."</li>
061: * <li>maxEntries - How many entries to show at most.</li>
062: * </ul>
063: * <p>The "days" and "startDate" can also be sent in HTTP parameters,
064: * and the names are "weblog.days" and "weblog.startDate", respectively.</p>
065: * <p>The weblog plugin also adds an attribute to each page it is on:
066: * "weblogplugin.isweblog" is set to "true". This can be used to quickly
067: * peruse pages which have weblogs.</p>
068: * @since 1.9.21
069: */
070:
071: // FIXME: Add "entries" param as an alternative to "days".
072: // FIXME: Entries arrive in wrong order.
073: public class WeblogPlugin implements WikiPlugin, ParserStagePlugin {
074: private static Logger log = Logger.getLogger(WeblogPlugin.class);
075: private static final DateFormat DEFAULT_ENTRYFORMAT = DateFormat
076: .getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT);
077: private static final Pattern headingPattern;
078:
079: /** How many days are considered by default. Default value is {@value} */
080: public static final int DEFAULT_DAYS = 7;
081: public static final String DEFAULT_PAGEFORMAT = "%p_blogentry_";
082:
083: public static final String DEFAULT_DATEFORMAT = "ddMMyy";
084:
085: public static final String PARAM_STARTDATE = "startDate";
086: public static final String PARAM_ENTRYFORMAT = "entryFormat";
087: public static final String PARAM_DAYS = "days";
088: public static final String PARAM_ALLOWCOMMENTS = "allowComments";
089: public static final String PARAM_MAXENTRIES = "maxEntries";
090: public static final String PARAM_PAGE = "page";
091:
092: public static final String ATTR_ISWEBLOG = "weblogplugin.isweblog";
093:
094: static {
095: // This is a pretty ugly, brute-force regex. But it will do for now...
096: headingPattern = Pattern.compile("(<h[1-4].*>)(.*)(</h[1-4]>)",
097: Pattern.CASE_INSENSITIVE);
098: }
099:
100: public static String makeEntryPage(String pageName, String date,
101: String entryNum) {
102: return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p",
103: pageName)
104: + date + "_" + entryNum;
105: }
106:
107: public static String makeEntryPage(String pageName) {
108: return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p",
109: pageName);
110: }
111:
112: public static String makeEntryPage(String pageName, String date) {
113: return TextUtil.replaceString(DEFAULT_PAGEFORMAT, "%p",
114: pageName)
115: + date;
116: }
117:
118: public String execute(WikiContext context, Map params)
119: throws PluginException {
120: Calendar startTime;
121: Calendar stopTime;
122: int numDays = DEFAULT_DAYS;
123: WikiEngine engine = context.getEngine();
124: AuthorizationManager mgr = engine.getAuthorizationManager();
125:
126: //
127: // Parse parameters.
128: //
129: String days;
130: DateFormat entryFormat;
131: String startDay = null;
132: boolean hasComments = false;
133: int maxEntries;
134: String weblogName;
135:
136: if ((weblogName = (String) params.get(PARAM_PAGE)) == null) {
137: weblogName = context.getPage().getName();
138: }
139:
140: if ((days = context.getHttpParameter("weblog." + PARAM_DAYS)) == null) {
141: days = (String) params.get(PARAM_DAYS);
142: }
143:
144: if ((params.get(PARAM_ENTRYFORMAT)) == null) {
145: entryFormat = DEFAULT_ENTRYFORMAT;
146: } else {
147: entryFormat = new SimpleDateFormat((String) params
148: .get(PARAM_ENTRYFORMAT));
149: }
150:
151: if (days != null) {
152: if (days.equalsIgnoreCase("all")) {
153: numDays = Integer.MAX_VALUE;
154: } else {
155: numDays = TextUtil
156: .parseIntParameter(days, DEFAULT_DAYS);
157: }
158: }
159:
160: if ((startDay = (String) params.get(PARAM_STARTDATE)) == null) {
161: startDay = context.getHttpParameter("weblog."
162: + PARAM_STARTDATE);
163: }
164:
165: if (TextUtil.isPositive((String) params
166: .get(PARAM_ALLOWCOMMENTS))) {
167: hasComments = true;
168: }
169:
170: maxEntries = TextUtil.parseIntParameter((String) params
171: .get(PARAM_MAXENTRIES), Integer.MAX_VALUE);
172:
173: //
174: // Determine the date range which to include.
175: //
176:
177: startTime = Calendar.getInstance();
178: stopTime = Calendar.getInstance();
179:
180: if (startDay != null) {
181: SimpleDateFormat fmt = new SimpleDateFormat(
182: DEFAULT_DATEFORMAT);
183: try {
184: Date d = fmt.parse(startDay);
185: startTime.setTime(d);
186: stopTime.setTime(d);
187: } catch (ParseException e) {
188: return "Illegal time format: " + startDay;
189: }
190: }
191:
192: //
193: // Mark this to be a weblog
194: //
195:
196: context.getPage().setAttribute(ATTR_ISWEBLOG, "true");
197:
198: //
199: // We make a wild guess here that nobody can do millisecond
200: // accuracy here.
201: //
202: startTime.add(Calendar.DAY_OF_MONTH, -numDays);
203: startTime.set(Calendar.HOUR, 0);
204: startTime.set(Calendar.MINUTE, 0);
205: startTime.set(Calendar.SECOND, 0);
206: stopTime.set(Calendar.HOUR, 23);
207: stopTime.set(Calendar.MINUTE, 59);
208: stopTime.set(Calendar.SECOND, 59);
209:
210: StringBuffer sb = new StringBuffer();
211:
212: try {
213: List blogEntries = findBlogEntries(engine.getPageManager(),
214: weblogName, startTime.getTime(), stopTime.getTime());
215:
216: Collections.sort(blogEntries, new PageDateComparator());
217:
218: sb.append("<div class=\"weblog\">\n");
219:
220: for (Iterator i = blogEntries.iterator(); i.hasNext()
221: && maxEntries-- > 0;) {
222: WikiPage p = (WikiPage) i.next();
223:
224: if (mgr.checkPermission(context.getWikiSession(),
225: new PagePermission(p,
226: PagePermission.VIEW_ACTION))) {
227: addEntryHTML(context, entryFormat, hasComments, sb,
228: p);
229: }
230: }
231:
232: sb.append("</div>\n");
233: } catch (ProviderException e) {
234: log.error("Could not locate blog entries", e);
235: throw new PluginException("Could not locate blog entries: "
236: + e.getMessage());
237: }
238:
239: return sb.toString();
240: }
241:
242: /**
243: * Generates HTML for an entry.
244: *
245: * @param context
246: * @param entryFormat
247: * @param hasComments True, if comments are enabled.
248: * @param buffer The buffer to which we add.
249: * @param entry
250: * @throws ProviderException
251: */
252: private void addEntryHTML(WikiContext context,
253: DateFormat entryFormat, boolean hasComments,
254: StringBuffer buffer, WikiPage entry)
255: throws ProviderException {
256: WikiEngine engine = context.getEngine();
257: buffer.append("<div class=\"weblogentry\">\n");
258:
259: //
260: // Heading
261: //
262: buffer.append("<div class=\"weblogentryheading\">\n");
263:
264: Date entryDate = entry.getLastModified();
265: buffer.append(entryFormat.format(entryDate));
266:
267: buffer.append("</div>\n");
268:
269: //
270: // Append the text of the latest version. Reset the
271: // context to that page.
272: //
273:
274: WikiContext entryCtx = (WikiContext) context.clone();
275: entryCtx.setPage(entry);
276:
277: String html = engine.getHTML(entryCtx, engine.getPage(entry
278: .getName()));
279:
280: // Extract the first h1/h2/h3 as title, and replace with null
281: buffer.append("<div class=\"weblogentrytitle\">\n");
282: Matcher matcher = headingPattern.matcher(html);
283: if (matcher.find()) {
284: String title = matcher.group(2);
285: html = matcher.replaceFirst("");
286: buffer.append(title);
287: } else {
288: buffer.append(entry.getName());
289: }
290: buffer.append("</div>\n");
291:
292: buffer.append("<div class=\"weblogentrybody\">\n");
293: buffer.append(html);
294: buffer.append("</div>\n");
295:
296: //
297: // Append footer
298: //
299: buffer.append("<div class=\"weblogentryfooter\">\n");
300:
301: String author = entry.getAuthor();
302:
303: if (author != null) {
304: if (engine.pageExists(author)) {
305: author = "<a href=\""
306: + entryCtx.getURL(WikiContext.VIEW, author)
307: + "\">" + engine.beautifyTitle(author) + "</a>";
308: }
309: } else {
310: author = "AnonymousCoward";
311: }
312:
313: buffer.append("By " + author + " ");
314: buffer.append("<a href=\""
315: + entryCtx.getURL(WikiContext.VIEW, entry.getName())
316: + "\">Permalink</a>");
317: String commentPageName = TextUtil.replaceString(
318: entry.getName(), "blogentry", "comments");
319:
320: if (hasComments) {
321: int numComments = guessNumberOfComments(engine,
322: commentPageName);
323:
324: //
325: // We add the number of comments to the URL so that
326: // the user's browsers would realize that the page
327: // has changed.
328: //
329: buffer.append(" ");
330: buffer.append("<a target=\"_blank\" href=\""
331: + entryCtx.getURL(WikiContext.COMMENT,
332: commentPageName, "nc=" + numComments)
333: + "\">Comments? (" + numComments + ")</a>");
334: }
335:
336: buffer.append("</div>\n");
337:
338: //
339: // Done, close
340: //
341: buffer.append("</div>\n");
342: }
343:
344: private int guessNumberOfComments(WikiEngine engine,
345: String commentpage) throws ProviderException {
346: String pagedata = engine.getPureText(commentpage,
347: WikiProvider.LATEST_VERSION);
348:
349: if (pagedata == null || pagedata.trim().length() == 0) {
350: return 0;
351: }
352:
353: return TextUtil.countSections(pagedata);
354: }
355:
356: /**
357: * Attempts to locate all pages that correspond to the
358: * blog entry pattern. Will only consider the days on the dates; not the hours and minutes.
359: *
360: * @param mgr A PageManager which is used to get the pages
361: * @param baseName The basename (e.g. "Main" if you want "Main_blogentry_xxxx")
362: * @param start The date which is the first to be considered
363: * @param end The end date which is the last to be considered
364: * @return a list of pages with their FIRST revisions.
365: * @throws ProviderException If something goes wrong
366: */
367: public List findBlogEntries(PageManager mgr, String baseName,
368: Date start, Date end) throws ProviderException {
369: Collection everyone = mgr.getAllPages();
370: ArrayList result = new ArrayList();
371:
372: baseName = makeEntryPage(baseName);
373: SimpleDateFormat fmt = new SimpleDateFormat(DEFAULT_DATEFORMAT);
374:
375: for (Iterator i = everyone.iterator(); i.hasNext();) {
376: WikiPage p = (WikiPage) i.next();
377:
378: String pageName = p.getName();
379:
380: if (pageName.startsWith(baseName)) {
381: //
382: // Check the creation date from the page name.
383: // We do this because RCSFileProvider is very slow at getting a
384: // specific page version.
385: //
386: try {
387: //log.debug("Checking: "+pageName);
388: int firstScore = pageName.indexOf('_', baseName
389: .length() - 1);
390: if (firstScore != -1
391: && firstScore + 1 < pageName.length()) {
392: int secondScore = pageName.indexOf('_',
393: firstScore + 1);
394:
395: if (secondScore != -1) {
396: String creationDate = pageName.substring(
397: firstScore + 1, secondScore);
398:
399: //log.debug(" Creation date: "+creationDate);
400:
401: Date pageDay = fmt.parse(creationDate);
402:
403: //
404: // Add the first version of the page into the list. This way
405: // the page modified date becomes the page creation date.
406: //
407: if (pageDay != null && pageDay.after(start)
408: && pageDay.before(end)) {
409: WikiPage firstVersion = mgr
410: .getPageInfo(pageName, 1);
411: result.add(firstVersion);
412: }
413: }
414: }
415: } catch (Exception e) {
416: log
417: .debug(
418: "Page name :"
419: + pageName
420: + " was suspected as a blog entry but it isn't because of parsing errors",
421: e);
422: }
423: }
424: }
425:
426: return result;
427: }
428:
429: /**
430: * Reverse comparison.
431: */
432: private static class PageDateComparator implements Comparator {
433: public int compare(Object o1, Object o2) {
434: if (o1 == null || o2 == null) {
435: return 0;
436: }
437:
438: WikiPage page1 = (WikiPage) o1;
439: WikiPage page2 = (WikiPage) o2;
440:
441: return page2.getLastModified().compareTo(
442: page1.getLastModified());
443: }
444: }
445:
446: /**
447: * Mark us as being a real weblog.
448: * {@inheritDoc}
449: */
450: public void executeParser(PluginContent element,
451: WikiContext context, Map params) {
452: context.getPage().setAttribute(ATTR_ISWEBLOG, "true");
453: }
454: }
|