blob: 41edac715197c540b2ad8de10d26dd412d79438a [file] [log] [blame]
package com.ecyrd.jspwiki.action;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;
import net.sourceforge.stripes.action.RedirectResolution;
import net.sourceforge.stripes.mock.MockHttpServletRequest;
import net.sourceforge.stripes.mock.MockHttpServletResponse;
import net.sourceforge.stripes.mock.MockHttpSession;
import net.sourceforge.stripes.util.ResolverUtil;
import org.apache.jspwiki.api.WikiException;
import org.apache.jspwiki.api.WikiPage;
import com.ecyrd.jspwiki.*;
import com.ecyrd.jspwiki.auth.SessionMonitor;
import com.ecyrd.jspwiki.content.ContentManager;
import com.ecyrd.jspwiki.content.WikiName;
import com.ecyrd.jspwiki.log.Logger;
import com.ecyrd.jspwiki.log.LoggerFactory;
import com.ecyrd.jspwiki.parser.MarkupParser;
import com.ecyrd.jspwiki.providers.ProviderException;
import com.ecyrd.jspwiki.tags.WikiTagBase;
import com.ecyrd.jspwiki.ui.stripes.HandlerInfo;
import com.ecyrd.jspwiki.ui.stripes.WikiActionBeanContext;
import com.ecyrd.jspwiki.url.StripesURLConstructor;
import com.ecyrd.jspwiki.util.TextUtil;
/**
* <p>
* Class that looks up {@link WikiActionBean}s, and resolves special pages and
* JSPs on behalf of a WikiEngine. WikiActionBeanResolver will automatically
* resolve page names with singular/plural variants. It can also detect the
* correct WikiActionBean based on parameters supplied in an HTTP request, or
* due to the JSP being accessed.
* </p>
*
* @author Andrew Jaquith
* @since 2.4.22
*/
public final class WikiContextFactory
{
/**
* The PageContext attribute name of the WikiEngine stored by
* WikiInterceptor.
*/
public static final String ATTR_WIKIENGINE = "wikiEngine";
/**
* The PageContext attribute name of the WikiSession stored by
* WikiInterceptor.
*/
public static final String ATTR_WIKISESSION = "wikiSession";
/** Default list of packages to search for WikiActionBean implementations. */
public static final String DEFAULT_ACTIONBEAN_PACKAGES = "com.ecyrd.jspwiki.action";
/**
* Property in jspwiki.properties that specifies packages to search for
* WikiActionBean implementations.
*/
public static final String PROPS_ACTIONBEAN_PACKAGES = "jspwiki.actionBean.packages";
private static final Logger log = LoggerFactory.getLogger( WikiContextFactory.class );
private static final long serialVersionUID = 1L;
/** Prefix in jspwiki.properties signifying special page keys. */
private static final String PROP_SPECIALPAGE = "jspwiki.specialPage.";
/**
* This method can be used to find the WikiContext programmatically from a
* JSP PageContext. We check the request context. The wiki context, if it
* exists, is looked up using the key
* {@link com.ecyrd.jspwiki.tags.WikiTagBase#ATTR_CONTEXT}.
*
* @since 2.4
* @param pageContext the JSP page context
* @return Current WikiContext, or null, of no context exists.
*/
public static WikiContext findContext( PageContext pageContext )
{
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
WikiContext context = (WikiContext) request.getAttribute( WikiTagBase.ATTR_CONTEXT );
return context;
}
/**
* <p>
* Saves the supplied WikiContext, and the related WikiEngine and
* WikiSession, in request scope. The WikiContext is saved as an attribute
* named {@link com.ecyrd.jspwiki.tags.WikiTagBase#ATTR_CONTEXT}. The
* WikiEngine is also saved as {@link #ATTR_WIKIENGINE}, and the
* WikiSession as {@link #ATTR_WIKISESSION}. Among other things, by saving
* these items as attributes, they can be accessed via JSP Expression
* Language variables, for example <code>${wikiContext}</code>.
* </p>
* <p>
* Note: when the WikiContext is saved, it will be guaranteed to have a
* non-null WikiPage. If the context as supplied has a WikiPage that is
* <code>null</code>, the
* {@link com.ecyrd.jspwiki.WikiEngine#getFrontPage()} will be consulted,
* and that page will be used.
* </p>
*
* @param request the HTTP request
* @param context the WikiContext to save
* @throws WikiException
*/
public static void saveContext( HttpServletRequest request, WikiContext context ) throws WikiException
{
// Stash WikiEngine as a request attribute (can be
// used later as ${wikiEngine} in EL markup)
WikiEngine engine = context.getEngine();
request.setAttribute( ATTR_WIKIENGINE, engine );
// Stash the WikiSession as a request attribute
WikiSession wikiSession = SessionMonitor.getInstance( engine ).find( request.getSession() );
request.setAttribute( ATTR_WIKISESSION, wikiSession );
WikiPage page = context.getPage();
if( page == null )
{
// If the page supplied was blank, default to the front page to
// avoid NPEs
page = engine.getFrontPage( ContentManager.DEFAULT_SPACE );
context.setPage( page );
}
request.setAttribute( WikiTagBase.ATTR_CONTEXT, context );
}
/** Private map with JSPs as keys, Resolutions as values */
private final Map<String, RedirectResolution> m_specialRedirects;
private final WikiEngine m_engine;
private String m_mockContextPath;
/** If true, we'll also consider english plurals (+s) a match. */
private boolean m_matchEnglishPlurals;
/** Maps (pre-3.0) request contexts map to WikiActionBeans. */
private final Map<String, HandlerInfo> m_contextMap = new HashMap<String, HandlerInfo>();
/**
* Constructs a WikiActionBeanResolver for a given WikiEngine. This
* constructor will extract the special page references for this wiki and
* store them in a cache used for resolution.
*
* @param engine the wiki engine
* @param properties the properties used to initialize the wiki
*/
public WikiContextFactory( WikiEngine engine, Properties properties )
{
super();
m_engine = engine;
m_specialRedirects = new HashMap<String, RedirectResolution>();
initRequestContextMap( properties );
initSpecialPageRedirects( properties );
// Do we match plurals?
m_matchEnglishPlurals = TextUtil.getBooleanProperty( properties, WikiEngine.PROP_MATCHPLURALS, true );
// Set the path prefix for constructing synthetic Stripes mock requests;
// trailing slash is removed.
m_mockContextPath = StripesURLConstructor.getContextPath( engine );
// TODO: make packages to search in ActionBeanResolver configurable
// (currently hard-coded)
}
/**
* Looks up and returns the correct HandlerInfo class corresponding to a
* supplied wiki context. The supplied context name is matched against the
* values annotated using
* {@link com.ecyrd.jspwiki.ui.stripes.WikiRequestContext}. If a match is not
* found, this method throws an IllegalArgumentException.
*
* @param requestContext the context to look up
* @return the WikiActionBean event handler info
*/
public HandlerInfo findEventHandler( String requestContext )
{
HandlerInfo handler = m_contextMap.get( requestContext );
if( handler == null )
{
throw new IllegalArgumentException( "No HandlerInfo found for request context '" + requestContext + "'!" );
}
return handler;
}
/**
* <p>
* Returns the correct page name, or <code>null</code>, if no such page
* can be found. Aliases are considered.
* </p>
* <p>
* In some cases, page names can refer to other pages. For example, when you
* have matchEnglishPlurals set, then a page name "Foobars" will be
* transformed into "Foobar", should a page "Foobars" not exist, but the
* page "Foobar" would. This method gives you the correct page name to refer
* to.
* </p>
* <p>
* This facility can also be used to rewrite any page name, for example, by
* using aliases. It can also be used to check the existence of any page.
* </p>
*
* @since 2.4.20
* @param page the page name.
* @return The rewritten page name, or <code>null</code>, if the page
* does not exist.
*/
public final String getFinalPageName( String page ) throws ProviderException
{
boolean isThere = simplePageExists( page );
String finalName = page;
if( !isThere && m_matchEnglishPlurals )
{
if( page.endsWith( "s" ) )
{
finalName = page.substring( 0, page.length() - 1 );
}
else
{
finalName += "s";
}
isThere = simplePageExists( finalName );
}
if( !isThere )
{
finalName = MarkupParser.wikifyLink( page );
isThere = simplePageExists( finalName );
if( !isThere && m_matchEnglishPlurals )
{
if( finalName.endsWith( "s" ) )
{
finalName = finalName.substring( 0, finalName.length() - 1 );
}
else
{
finalName += "s";
}
isThere = simplePageExists( finalName );
}
}
return isThere ? finalName : null;
}
/**
* <p>
* If the page is a special page, this method returns a
* {@link net.sourceforge.stripes.action.RedirectResolution} for that page;
* otherwise, it returns <code>null</code>.
* </p>
* <p>
* Special pages are non-existent references to other pages. For example,
* you could define a special page reference "RecentChanges" which would
* always be redirected to "RecentChanges.jsp" instead of trying to find a
* Wiki page called "RecentChanges".
* </p>
* TODO: fix this algorithm
*/
public final RedirectResolution getSpecialPageResolution( String page )
{
return m_specialRedirects.get( page );
}
/**
* <p>
* Creates a WikiActionBeanContext instance, associates an HTTP request and
* response with it, and sets the correct WikiPage into the context if
* required. This method will determine what page the user requested by
* delegating to {@link #extractPageFromParameter(HttpServletRequest)}.
* </p>
* <p>
* This method will <em>always</em>return a {@link WikiActionBeanContext}
* that is properly instantiated. The supplied request and response objects
* will be associated with the WikiActionBeanContext. The
* <code>requestContext</code>is required. If either the
* <code>request</code> or <code>response</code> parameters are
* <code>null</code>, appropriate mock objects will be substituted
* instead.
* </p>
* <p>
* This method performs a similar role to the Stripes
* {@link net.sourceforge.stripes.controller.ActionBeanContextFactory#getContextInstance(HttpServletRequest, HttpServletResponse)}
* method, in the sense that it will instantiate an arbitrary
* ActionBeanContext class. However, although this method will correctly
* identity the page requested by the user (by inspecting request
* parameters), it will not do anything special if the page is a "special
* page."
* </p>
*
* @param request the HTTP request
* @param response the HTTP request
* @param requestContext the request context to use by default
* @return the new WikiActionBeanContext
*/
public WikiActionBeanContext newContext( HttpServletRequest request, HttpServletResponse response, String requestContext )
throws WikiException
{
return createContext( requestContext, request, response, null );
}
/**
* <p>
* Creates a new WikiActionBeanContext for the given HttpServletRequest,
* HttpServletResponse and WikiPage, using the {@link WikiContext#VIEW}
* request context. Similar to method
* {@link #newContext(HttpServletRequest, HttpServletResponse, String)},
* this method will <em>always</em>return a {@link WikiActionBeanContext}
* that is properly instantiated. The supplied request and response objects
* will be associated with the WikiActionBeanContext. If either the
* <code>request</code> or <code>response</code> parameters are
* <code>null</code>, appropriate mock objects will be substituted
* instead.
* </p>
*
* @param request The HttpServletRequest that should be associated with this
* context. This parameter may be <code>null</code>.
* @param response The HttpServletResponse that should be associated with
* this context. This parameter may be <code>null</code>.
* @param page The WikiPage. If you want to create a WikiContext for an
* older version of a page, you must supply this parameter
* @return the new WikiActionBeanContext
*/
public WikiActionBeanContext newViewContext( HttpServletRequest request, HttpServletResponse response, WikiPage page )
{
// Create a new "view" WikiActionBeanContext, and swallow any exceptions
WikiActionBeanContext ctx = null;
try
{
ctx = createContext( WikiContext.VIEW, request, response, page );
if( ctx == null )
{
throw new IllegalStateException( "Could not create new WikiContext of type VIEW! This indicates a bug..." );
}
}
catch( WikiException e )
{
e.printStackTrace();
log.error( e.getMessage() );
}
return ctx;
}
/**
* Provides a clean shortcut to newViewContext(null,null,page).
*
* @param page The WikiPage object for this page.
* @return A new WikiPage.
*/
public WikiActionBeanContext newViewContext( WikiPage page )
{
return newViewContext( null, null, page );
}
/**
* Searches a set of named packages for WikiActionBean implementations, and
* returns any it finds.
*
* @param beanPackages the packages to search on the current classpath,
* separated by commas
* @return the discovered classes
*/
private Set<Class<? extends WikiActionBean>> findBeanClasses( String[] beanPackages )
{
ResolverUtil<WikiActionBean> resolver = new ResolverUtil<WikiActionBean>();
resolver.findImplementations( WikiActionBean.class, beanPackages );
return resolver.getClasses();
}
/**
* Initializes the internal map that matches wiki request contexts with
* HandlerInfo objects.
*
* @param properties
*/
private void initRequestContextMap( Properties properties )
{
// Look up all classes that are WikiActionBeans.
String beanPackagesProp = properties.getProperty( PROPS_ACTIONBEAN_PACKAGES, DEFAULT_ACTIONBEAN_PACKAGES ).trim();
String[] beanPackages = beanPackagesProp.split( "," );
Set<Class<? extends WikiActionBean>> beanClasses = findBeanClasses( beanPackages );
// Stash the contexts and corresponding classes into a Map.
for( Class<? extends WikiActionBean> beanClass : beanClasses )
{
Map<Method, HandlerInfo> handlerMethods = HandlerInfo.getHandlerInfoCollection( beanClass );
for( HandlerInfo handler : handlerMethods.values() )
{
String requestContext = handler.getRequestContext();
if( m_contextMap.containsKey( requestContext ) )
{
HandlerInfo duplicateHandler = m_contextMap.get( requestContext );
log.error( "Bean class " + beanClass.getCanonicalName() + " contains @WikiRequestContext annotation '"
+ requestContext + "' that duplicates one already declared for "
+ duplicateHandler.getActionBeanClass() );
}
else
{
m_contextMap.put( requestContext, handler );
log.info( "Discovered request context '" + requestContext + "' for WikiActionBean="
+ beanClass.getCanonicalName() + ",event=" + handler.getEventName() );
}
}
}
}
/**
* Skims through a supplied set of Properties and looks for anything with
* the "special page" prefix, and creates Stripes
* {@link net.sourceforge.stripes.action.RedirectResolution} objects for any
* that are found.
*/
private void initSpecialPageRedirects( Properties properties )
{
for( Map.Entry entry : properties.entrySet() )
{
String key = (String) entry.getKey();
if( key.startsWith( PROP_SPECIALPAGE ) )
{
String specialPage = key.substring( PROP_SPECIALPAGE.length() );
String redirectUrl = (String) entry.getValue();
if( specialPage != null && redirectUrl != null )
{
specialPage = specialPage.trim();
// Parse the special page
redirectUrl = redirectUrl.trim();
try
{
URI uri = new URI( redirectUrl );
if ( uri.getAuthority() == null )
{
// No http:// ftp:// or other authority, so it must be relative to webapp /
if ( !redirectUrl.startsWith( "/" ) )
{
redirectUrl = "/" + redirectUrl;
}
}
}
catch( URISyntaxException e )
{
// The user supplied a STRANGE reference
log.error( "Strange special page reference: " + redirectUrl );
}
// Add a new RedirectResolution for the special page
RedirectResolution resolution = m_specialRedirects.get( specialPage );
if( resolution == null )
{
resolution = new RedirectResolution( redirectUrl );
m_specialRedirects.put( specialPage, resolution );
}
}
}
}
}
/**
* Creates and returns a new WikiActionBean based on a supplied class, with
* an instantiated {@link WikiActionBeanContext}. The
* WikiActionBeanContext's request and response properties will be set to
* those supplied by the caller; if not supplied, synthetic instances will
* be substituted.
*
* @param requestContext the request context to use by default
* @param request
* @param response
* @return the newly instantiated bean
*/
protected WikiActionBeanContext createContext( String requestContext, HttpServletRequest request, HttpServletResponse response,
WikiPage page ) throws WikiException
{
// Create synthetic request if not supplied
if( request == null )
{
request = new MockHttpServletRequest( m_mockContextPath, "/Wiki.jsp" );
MockHttpSession session = new MockHttpSession( m_engine.getServletContext() );
((MockHttpServletRequest) request).setSession( session );
}
// Create synthetic response if not supplied
if( response == null )
{
response = new MockHttpServletResponse();
}
// Create the WikiActionBeanContext and set all of its relevant
// properties
WikiActionBeanContext context = new WikiActionBeanContext();
context.setRequest( request );
context.setResponse( response );
context.setEngine( m_engine );
context.setServletContext( m_engine.getServletContext() );
WikiSession wikiSession = SessionMonitor.getInstance( m_engine ).find( request.getSession() );
context.setWikiSession( wikiSession );
// Set the request context (and related event name)
context.setRequestContext( requestContext );
// Extract and set the WikiPage
if( page == null )
{
String pageName = extractPageFromParameter( request );
// For view action, default to front page
if( pageName == null && WikiContext.VIEW.equals( requestContext ) )
{
pageName = m_engine.getFrontPage();
}
// Make sure the page is resolved properly (taking into account
// funny plurals)
if( pageName != null )
{
page = resolvePage( request, pageName );
}
}
if( page != null )
{
context.setPage( page );
}
return context;
}
/**
* <p>
* Determines the correct wiki page based on a supplied HTTP request. This
* method attempts to determine the page requested by a user, taking into
* account special pages. The resolution algorithm will extract the page
* name from the URL by looking for the first parameter value returned for
* the <code>page</code> parameter. If a page name was, in fact, passed in
* the request, this method the correct name after taking into account
* potential plural matches.
* </p>
* <p>
* If neither of these methods work, or if the request is <code>null</code>
* this method returns <code>null</code>
* </p>.
*
* @param request the HTTP request
* @return the resolved page name
*/
protected final String extractPageFromParameter( HttpServletRequest request )
{
// Corner case when request == null
if( request == null )
{
return null;
}
// Extract the page name from the URL directly
String[] pages = request.getParameterValues( "page" );
String page = null;
if( pages != null && pages.length > 0 )
{
page = pages[0];
try
{
// Look for singular/plural variants; if one
// not found, take the one the user supplied
String finalPage = getFinalPageName( page );
if( finalPage != null )
{
page = finalPage;
}
}
catch( ProviderException e )
{
// FIXME: Should not ignore!
}
return page;
}
// Didn't resolve; return null
return page;
}
/**
* Looks up and returns the correct, versioned WikiPage based on a supplied
* page name and optional <code>version</code> parameter passed in an HTTP
* request. If the <code>version</code> parameter does not exist in the
* request, the latest version is returned.
*
* @param request the HTTP request
* @param page the name of the page to look up; this page <em>must</em>
* exist
* @return the wiki page
* @throws WikiException
*/
protected final WikiPage resolvePage( HttpServletRequest request, String page ) throws WikiException
{
// See if the user included a version parameter
WikiPage wikipage;
int version = WikiProvider.LATEST_VERSION;
String rev = request.getParameter( "version" );
if( rev != null )
{
version = Integer.parseInt( rev );
}
wikipage = m_engine.getPage( page, version );
if( wikipage == null )
{
page = MarkupParser.cleanLink( page );
wikipage = m_engine.createPage( WikiName.valueOf( page ) );
}
return wikipage;
}
/**
* Determines whether a "page" exists by examining the list of special pages
* and querying the page manager.
*
* @param page the page to seek
* @return <code>true</code> if the page exists, <code>false</code>
* otherwise
*/
protected final boolean simplePageExists( String page ) throws ProviderException
{
if( m_specialRedirects.containsKey( page ) )
{
return true;
}
return m_engine.getPageManager().pageExists( page );
}
/**
* Creates an "empty" wiki context which is used internally sometimes when
* access to the repository is desired outside of a request. This is
* really just shorthand of getting a new VIEW context with an imaginary
* page.
*
* @return A valid WikiContext.
* @throws WikiException
*/
// TODO: Should this be better called "newVirtualContext()" or something? "Empty" is not
// very descriptive.
public WikiContext newEmptyContext() throws WikiException
{
WikiPage page = m_engine.createPage( "__DUMMY__");
return newViewContext( null, null, page );
}
}