001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2007 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.ui;
021:
022: import java.io.IOException;
023: import java.util.HashMap;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.Properties;
027:
028: import javax.servlet.http.HttpServletRequest;
029:
030: import org.apache.log4j.Logger;
031:
032: import com.ecyrd.jspwiki.InternalWikiException;
033: import com.ecyrd.jspwiki.TextUtil;
034: import com.ecyrd.jspwiki.WikiEngine;
035: import com.ecyrd.jspwiki.WikiPage;
036: import com.ecyrd.jspwiki.WikiProvider;
037: import com.ecyrd.jspwiki.auth.GroupPrincipal;
038: import com.ecyrd.jspwiki.parser.MarkupParser;
039: import com.ecyrd.jspwiki.providers.ProviderException;
040: import com.ecyrd.jspwiki.url.URLConstructor;
041:
042: /**
043: * <p>Resolves special pages, JSPs and Commands on behalf of a
044: * WikiEngine. CommandResolver will automatically resolve page names
045: * with singular/plural variants. It can also detect the correct Command
046: * based on parameters supplied in an HTTP request, or due to the
047: * JSP being accessed.</p>
048: * <p>
049: * <p>CommandResolver's static {@link #findCommand(String)} method is
050: * the simplest method; it looks up and returns the Command matching
051: * a supplied wiki context. For example, looking up the request context
052: * <code>view</code> returns {@link PageCommand#VIEW}. Use this method
053: * to obtain static Command instances that aren't targeted at a particular
054: * page or group.</p>
055: * <p>For more complex lookups in which the caller supplies an HTTP
056: * request, {@link #findCommand(HttpServletRequest, String)} will
057: * look up and return the correct Command. The String parameter
058: * <code>defaultContext</code> supplies the request context to use
059: * if it cannot be detected. However, note that the default wiki
060: * context may be over-ridden if the request was for a "special page."</p>
061: * <p>For example, suppose the WikiEngine's properties specify a
062: * special page called <code>UserPrefs</code>
063: * that redirects to <code>UserPreferences.jsp</code>. The ordinary
064: * lookup method {@linkplain #findCommand(String)} using a supplied
065: * context <code>view</code> would return {@link PageCommand#VIEW}. But
066: * the {@linkplain #findCommand(HttpServletRequest, String)} method,
067: * when passed the same context (<code>view</code>) and an HTTP request
068: * containing the page parameter value <code>UserPrefs</code>,
069: * will instead return {@link WikiCommand#PREFS}.</p>
070: * @author Andrew Jaquith
071: * @since 2.4.22
072: */
073: public final class CommandResolver {
074: /** Prefix in jspwiki.properties signifying special page keys. */
075: private static final String PROP_SPECIALPAGE = "jspwiki.specialPage.";
076:
077: /** Private map with request contexts as keys, Commands as values */
078: private static final Map CONTEXTS;
079:
080: /** Private map with JSPs as keys, Commands as values */
081: private static final Map JSPS;
082:
083: /** Store the JSP-to-Command and context-to-Command mappings */
084: static {
085: CONTEXTS = new HashMap();
086: JSPS = new HashMap();
087: Command[] commands = AbstractCommand.allCommands();
088: for (int i = 0; i < commands.length; i++) {
089: JSPS.put(commands[i].getJSP(), commands[i]);
090: CONTEXTS.put(commands[i].getRequestContext(), commands[i]);
091: }
092: }
093:
094: private final Logger m_log = Logger
095: .getLogger(CommandResolver.class);
096:
097: private final WikiEngine m_engine;
098:
099: /** If true, we'll also consider english plurals (+s) a match. */
100: private final boolean m_matchEnglishPlurals;
101:
102: /** Stores special page names as keys, and Commands as values. */
103: private final Map m_specialPages;
104:
105: /**
106: * Constructs a CommandResolver for a given WikiEngine. This constructor
107: * will extract the special page references for this wiki and store them in
108: * a cache used for resolution.
109: * @param engine the wiki engine
110: * @param properties the properties used to initialize the wiki
111: */
112: public CommandResolver(WikiEngine engine, Properties properties) {
113: m_engine = engine;
114: m_specialPages = new HashMap();
115:
116: // Skim through the properties and look for anything with
117: // the "special page" prefix. Create maps that allow us
118: // look up the correct Command based on special page name.
119: // If a matching command isn't found, create a RedirectCommand.
120: for (Iterator i = properties.entrySet().iterator(); i.hasNext();) {
121: Map.Entry entry = (Map.Entry) i.next();
122: String key = (String) entry.getKey();
123: if (key.startsWith(PROP_SPECIALPAGE)) {
124: String specialPage = key.substring(PROP_SPECIALPAGE
125: .length());
126: String jsp = (String) entry.getValue();
127: if (specialPage != null && jsp != null) {
128: specialPage = specialPage.trim();
129: jsp = jsp.trim();
130: Command command = (Command) JSPS.get(jsp);
131: if (command == null) {
132: Command redirect = RedirectCommand.REDIRECT;
133: command = redirect.targetedCommand(jsp);
134: }
135: m_specialPages.put(specialPage, command);
136: }
137: }
138: }
139:
140: // Do we match plurals?
141: m_matchEnglishPlurals = TextUtil.getBooleanProperty(properties,
142: WikiEngine.PROP_MATCHPLURALS, true);
143: }
144:
145: /**
146: * Attempts to locate a wiki command for a supplied request context.
147: * The resolution technique is simple: we examine the list of
148: * Commands returned by {@link AbstractCommand#allCommands()} and
149: * return the one whose <code>requestContext</code> matches the
150: * supplied context. If the supplied context does not resolve to a known
151: * Command, this method throws an {@link IllegalArgumentException}.
152: * @param context the request context
153: * @return the resolved context
154: */
155: public static Command findCommand(String context) {
156: Command command = (Command) CONTEXTS.get(context);
157: if (command == null) {
158: throw new IllegalArgumentException(
159: "Unsupported wiki context: " + context + ".");
160: }
161: return command;
162: }
163:
164: /**
165: * <p>
166: * Attempts to locate a Command for a supplied wiki context and HTTP
167: * request, incorporating the correct WikiPage into the command if reqiured.
168: * This method will first determine what page the user requested by
169: * delegating to {@link #extractPageFromParameter(String, HttpServletRequest)}. If
170: * this page equates to a special page, we return the Command
171: * corresponding to that page. Otherwise, this method simply returns the
172: * Command for the supplied request context.
173: * </p>
174: * <p>
175: * The reason this method attempts to resolve against special pages is
176: * because some of them resolve to contexts that may be different from the
177: * one supplied. For example, a VIEW request context for the special page
178: * "UserPreferences" should return a PREFS context instead.
179: * </p>
180: * <p>
181: * When the caller supplies a request context and HTTP request that
182: * specifies an actual wiki page (rather than a special page), this method
183: * will return a "targeted" Command that includes the resolved WikiPage
184: * as the target. (See {@link #resolvePage(HttpServletRequest, String)}
185: * for the resolution algorithm). Specifically, the Command will
186: * return a non-<code>null</code> value for its {@link AbstractCommand#getTarget()} method.
187: * </p>
188: * <p><em>Note: if this method determines that the Command is the VIEW PageCommand,
189: * then the Command returned will always be targeted to the front page.</em></p>
190: * @param request the HTTP request; if <code>null</code>, delegates
191: * to {@link #findCommand(String)}
192: * @param defaultContext the request context to use by default
193: * @return the resolved wiki command
194: */
195: public final Command findCommand(HttpServletRequest request,
196: String defaultContext) {
197: // Corner case if request is null
198: if (request == null) {
199: return findCommand(defaultContext);
200: }
201:
202: Command command = null;
203:
204: // Determine the name of the page (which may be null)
205: String pageName = extractPageFromParameter(defaultContext,
206: request);
207:
208: // Can we find a special-page command matching the extracted page?
209: if (pageName != null) {
210: command = (AbstractCommand) m_specialPages.get(pageName);
211: }
212:
213: // If we haven't found a matching command yet, extract the JSP path
214: // and compare to our list of special pages
215: if (command == null) {
216: command = extractCommandFromPath(request);
217:
218: // Otherwise: use the default context
219: if (command == null) {
220: command = (AbstractCommand) CONTEXTS
221: .get(defaultContext);
222: if (command == null) {
223: throw new IllegalArgumentException("Wiki context "
224: + defaultContext + " is illegal.");
225: }
226: }
227: }
228:
229: // For PageCommand.VIEW, default to front page if a page wasn't supplied
230: if (PageCommand.VIEW.equals(command) && pageName == null) {
231: pageName = m_engine.getFrontPage();
232: }
233:
234: // These next blocks handle targeting requirements
235:
236: // If we were passed a page parameter, try to resolve it
237: if (command instanceof PageCommand && pageName != null) {
238: // If there's a matching WikiPage, "wrap" the command
239: WikiPage page = resolvePage(request, pageName);
240: if (page != null) {
241: return command.targetedCommand(page);
242: }
243: }
244:
245: // If "create group" command, target this wiki
246: String wiki = m_engine.getApplicationName();
247: if (WikiCommand.CREATE_GROUP.equals(command)) {
248: return WikiCommand.CREATE_GROUP.targetedCommand(wiki);
249: }
250:
251: // If group command, see if we were passed a group name
252: if (command instanceof GroupCommand) {
253: String groupName = request.getParameter("group");
254: groupName = TextUtil.replaceEntities(groupName);
255: if (groupName != null && groupName.length() > 0) {
256: GroupPrincipal group = new GroupPrincipal(groupName);
257: return command.targetedCommand(group);
258: }
259: }
260:
261: // No page provided; return an "ordinary" command
262: return command;
263: }
264:
265: /**
266: * <p>
267: * Returns the correct page name, or <code>null</code>, if no such page can be found.
268: * Aliases are considered.
269: * </p>
270: * <p>
271: * In some cases, page names can refer to other pages. For example, when you
272: * have matchEnglishPlurals set, then a page name "Foobars" will be
273: * transformed into "Foobar", should a page "Foobars" not exist, but the
274: * page "Foobar" would. This method gives you the correct page name to refer
275: * to.
276: * </p>
277: * <p>
278: * This facility can also be used to rewrite any page name, for example, by
279: * using aliases. It can also be used to check the existence of any page.
280: * </p>
281: * @since 2.4.20
282: * @param page the page name.
283: * @return The rewritten page name, or <code>null</code>, if the page does not exist.
284: * @throws ProviderException if the underlyng page provider that locates pages
285: * throws an exception
286: */
287: public final String getFinalPageName(String page)
288: throws ProviderException {
289: boolean isThere = simplePageExists(page);
290: String finalName = page;
291:
292: if (!isThere && m_matchEnglishPlurals) {
293: if (page.endsWith("s")) {
294: finalName = page.substring(0, page.length() - 1);
295: } else {
296: finalName += "s";
297: }
298:
299: isThere = simplePageExists(finalName);
300: }
301:
302: if (!isThere) {
303: finalName = MarkupParser.wikifyLink(page);
304: isThere = simplePageExists(finalName);
305:
306: if (!isThere && m_matchEnglishPlurals) {
307: if (finalName.endsWith("s")) {
308: finalName = finalName.substring(0, finalName
309: .length() - 1);
310: } else {
311: finalName += "s";
312: }
313:
314: isThere = simplePageExists(finalName);
315: }
316: }
317:
318: return isThere ? finalName : null;
319: }
320:
321: /**
322: * <p>
323: * If the page is a special page, this method returns a direct URL to that
324: * page; otherwise, it returns <code>null</code>.
325: * </p>
326: * <p>
327: * Special pages are non-existant references to other pages. For example,
328: * you could define a special page reference "RecentChanges" which would
329: * always be redirected to "RecentChanges.jsp" instead of trying to find a
330: * Wiki page called "RecentChanges".
331: * </p>
332: * @param page the page name ro search for
333: * @return the URL of the special page, if the supplied page is one, or <code>null</code>
334: */
335: public final String getSpecialPageReference(String page) {
336: Command command = (Command) m_specialPages.get(page);
337:
338: if (command != null) {
339: return m_engine.getURLConstructor().makeURL(
340: command.getRequestContext(),
341: command.getURLPattern(), true, null);
342: }
343:
344: return null;
345: }
346:
347: /**
348: * Extracts a Command based on the JSP path of an HTTP request.
349: * If the JSP requested matches a Command's <code>getJSP()</code>
350: * value, that Command is returned.
351: * @param request the HTTP request
352: * @return the resolved Command, or <code>null</code> if not found
353: */
354: protected final Command extractCommandFromPath(
355: HttpServletRequest request) {
356: String jsp = request.getServletPath();
357:
358: // Take everything to right of initial / and left of # or ?
359: int hashMark = jsp.indexOf('#');
360: if (hashMark != -1) {
361: jsp = jsp.substring(0, hashMark);
362: }
363: int questionMark = jsp.indexOf('?');
364: if (questionMark != -1) {
365: jsp = jsp.substring(0, questionMark);
366: }
367: if (jsp.startsWith("/")) {
368: jsp = jsp.substring(1);
369: }
370:
371: // Find special page reference?
372: for (Iterator i = m_specialPages.entrySet().iterator(); i
373: .hasNext();) {
374: Map.Entry entry = (Map.Entry) i.next();
375: Command specialCommand = (Command) entry.getValue();
376: if (specialCommand.getJSP().equals(jsp)) {
377: return specialCommand;
378: }
379: }
380:
381: // Still haven't found a matching command?
382: // Ok, see if we match against our standard list of JSPs
383: if (jsp.length() > 0 && JSPS.containsKey(jsp)) {
384: return (Command) JSPS.get(jsp);
385: }
386:
387: return null;
388: }
389:
390: /**
391: * Determines the correct wiki page based on a supplied request context and
392: * HTTP request. This method attempts to determine the page requested by a
393: * user, taking into acccount special pages. The resolution algorithm will:
394: * <ul>
395: * <li>Extract the page name from the URL according to the rules for the
396: * current {@link URLConstructor}. If a page name was
397: * passed in the request, return the correct name after taking into account
398: * potential plural matches.</li>
399: * <li>If the extracted page name is <code>null</code>, attempt to see
400: * if a "special page" was intended by examining the servlet path. For
401: * example, the request path "/UserPreferences.jsp" will resolve to
402: * "UserPreferences."</li>
403: * <li>If neither of these methods work, this method returns
404: * <code>null</code></li>
405: * </ul>
406: * @param requestContext the request context
407: * @param request the HTTP request
408: * @return the resolved page name
409: */
410: protected final String extractPageFromParameter(
411: String requestContext, HttpServletRequest request) {
412: String page;
413:
414: // Extract the page name from the URL directly
415: try {
416: page = m_engine.getURLConstructor().parsePage(
417: requestContext, request,
418: m_engine.getContentEncoding());
419: if (page != null) {
420: try {
421: // Look for singular/plural variants; if one
422: // not found, take the one the user supplied
423: String finalPage = getFinalPageName(page);
424: if (finalPage != null) {
425: page = finalPage;
426: }
427: } catch (ProviderException e) {
428: // FIXME: Should not ignore!
429: }
430: return page;
431: }
432: } catch (IOException e) {
433: m_log.error("Unable to create context", e);
434: throw new InternalWikiException(
435: "Big internal booboo, please check logs.");
436: }
437:
438: // Didn't resolve; return null
439: return null;
440: }
441:
442: /**
443: * Looks up and returns the correct, versioned WikiPage based on a supplied
444: * page name and optional <code>version</code> parameter passed in an HTTP
445: * request. If the <code>version</code> parameter does not exist in the
446: * request, the latest version is returned.
447: * @param request the HTTP request
448: * @param page the name of the page to look up; this page <em>must</em> exist
449: * @return the wiki page
450: */
451: protected final WikiPage resolvePage(HttpServletRequest request,
452: String page) {
453: // See if the user included a version parameter
454: WikiPage wikipage;
455: int version = WikiProvider.LATEST_VERSION;
456: String rev = request.getParameter("version");
457:
458: if (rev != null) {
459: version = Integer.parseInt(rev);
460: }
461:
462: wikipage = m_engine.getPage(page, version);
463:
464: if (wikipage == null) {
465: page = MarkupParser.cleanLink(page);
466: wikipage = new WikiPage(m_engine, page);
467: }
468: return wikipage;
469: }
470:
471: /**
472: * Determines whether a "page" exists by examining the list of special pages
473: * and querying the page manager.
474: * @param page the page to seek
475: * @return <code>true</code> if the page exists, <code>false</code>
476: * otherwise
477: * @throws ProviderException if the underlyng page provider that locates pages
478: * throws an exception
479: */
480: protected final boolean simplePageExists(String page)
481: throws ProviderException {
482: if (m_specialPages.containsKey(page)) {
483: return true;
484: }
485: return m_engine.getPageManager().pageExists(page);
486: }
487:
488: }
|