blob: 5ab831dc3876a11530cf8185e3f9934cc9dc1223 [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.wicket.protocol.http.mock;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.ServletRegistration.Dynamic;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.descriptor.JspConfigDescriptor;
import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.cookies.CookieUtils;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.value.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Mock implementation of the servlet context for testing purposes. This implementation supports all
* of the standard context methods except that request dispatching just indicates what is being
* dispatched to, rather than doing the actual dispatch.
* <p>
* The context can be configured with a path parameter that should point to an absolute directory
* location that represents the place where the contents of the WAR bundle are located. Setting this
* value allows all of the resource location functionality to work as in a fully functioning web
* application. This value is not set then not resource location functionality will work and instead
* null will always be returned.
*
* @author Chris Turner
*/
public class MockServletContext implements ServletContext
{
private static final Logger log = LoggerFactory.getLogger(MockServletContext.class);
private final Application application;
private final ValueMap attributes = new ValueMap();
private final ValueMap initParameters = new ValueMap();
private final Map<String, ServletRegistration.Dynamic> servletRegistration = new HashMap<>();
/** Map of mime types */
private final ValueMap mimeTypes = new ValueMap();
private File webappRoot;
private final SessionCookieConfig sessionCookieConfig = new SessionCookieConfig()
{
private boolean secure;
private String path;
private String name = CookieUtils.DEFAULT_SESSIONID_COOKIE_NAME;
private int maxAge;
private boolean httpOnly;
private String domain;
private String comment;
@Override
public void setSecure(boolean secure)
{
this.secure = secure;
}
@Override
public void setPath(String path)
{
this.path = path;
}
@Override
public void setName(String name)
{
this.name = name;
}
@Override
public void setMaxAge(int maxAge)
{
this.maxAge = maxAge;
}
@Override
public void setHttpOnly(boolean httpOnly)
{
this.httpOnly = httpOnly;
}
@Override
public void setDomain(String domain)
{
this.domain = domain;
}
@Override
public void setComment(String comment)
{
this.comment = comment;
}
@Override
public boolean isSecure()
{
return secure;
}
@Override
public boolean isHttpOnly()
{
return httpOnly;
}
@Override
public String getPath()
{
return path;
}
@Override
public String getName()
{
return name;
}
@Override
public int getMaxAge()
{
return maxAge;
}
@Override
public String getDomain()
{
return domain;
}
@Override
public String getComment()
{
return comment;
}
};
private int sessionTimeout = 30; // in minutes
private Charset requestCharacterEncoding = StandardCharsets.UTF_8;
private Charset responseCharacterEncoding = StandardCharsets.UTF_8;
/**
* Create the mock object. As part of the creation, the context sets the root directory where
* web application content is stored. This must be an ABSOLUTE directory relative to where the
* tests are being executed. For example: <code>System.getProperty("user.dir") +
* "/src/webapp"</code>
*
* @param application
* The application that this context is for
* @param path
* The path to the root of the web application
*/
public MockServletContext(final Application application, final String path)
{
this.application = application;
webappRoot = null;
if (path != null)
{
webappRoot = new File(path);
if (!webappRoot.exists() || !webappRoot.isDirectory())
{
log.warn("WARNING: The webapp root directory is invalid: " + path);
webappRoot = null;
}
}
// the user app can configure specific work folder by setting -Dwicket.tester.work.folder JVM option,
// otherwise assume we're running in maven or an eclipse project created by maven,
// so the sessions directory will be created inside the target directory,
// and will be cleaned up with a mvn clean
String workFolder = System.getProperty("wicket.tester.work.folder", "target/work/");
File file = new File(workFolder);
try
{
file.mkdirs();
}
catch (SecurityException sx)
{
// not allowed to write so fallback to tmpdir
String tmpDir = System.getProperty("java.io.tmpdir");
file = new File(tmpDir);
}
attributes.put("jakarta.servlet.context.tempdir", file);
mimeTypes.put("html", "text/html");
mimeTypes.put("htm", "text/html");
mimeTypes.put("css", "text/css");
mimeTypes.put("xml", "text/xml");
mimeTypes.put("js", "text/javascript");
mimeTypes.put("gif", "image/gif");
mimeTypes.put("jpg", "image/jpeg");
mimeTypes.put("png", "image/png");
}
/**
* Add an init parameter.
*
* @param name
* The parameter name
* @param value
* The parameter value
*/
public void addInitParameter(final String name, final String value)
{
initParameters.put(name, value);
}
// Configuration methods
/**
* Add a new recognized mime type.
*
* @param fileExtension
* The file extension (e.g. "jpg")
* @param mimeType
* The mime type (e.g. "image/jpeg")
*/
public void addMimeType(final String fileExtension, final String mimeType)
{
mimeTypes.put(fileExtension, mimeType);
}
/**
* Get an attribute with the given name.
*
* @param name
* The attribute name
* @return The value, or null
*/
@Override
public Object getAttribute(final String name)
{
return attributes.get(name);
}
/**
* Get all of the attribute names.
*
* @return The attribute names
*/
@Override
public Enumeration<String> getAttributeNames()
{
return Collections.enumeration(attributes.keySet());
}
// ServletContext interface methods
/**
* Get the context for the given URL path
*
* @param name
* The url path
* @return Always returns this
*/
@Override
public ServletContext getContext(String name)
{
return this;
}
/**
* Get the init parameter with the given name.
*
* @param name
* The name
* @return The parameter, or null if no such parameter
*/
@Override
public String getInitParameter(final String name)
{
return initParameters.getString(name);
}
/**
* Get the name of all of the init parameters.
*
* @return The init parameter names
*/
@Override
public Enumeration<String> getInitParameterNames()
{
return Collections.enumeration(initParameters.keySet());
}
@Override
public boolean setInitParameter(String name, String value)
{
return false;
}
/**
* Get the mime type for the given file. Uses a hardcoded map of mime types set at
* Initialization time.
*
* @param name
* The name to get the mime type for
* @return The mime type
*/
@Override
public String getMimeType(final String name)
{
int index = name.lastIndexOf('.');
if (index == -1 || index == (name.length() - 1))
{
return null;
}
else
{
return mimeTypes.getString(name.substring(index + 1));
}
}
@Override
public int getMajorVersion()
{
return 3;
}
@Override
public int getMinorVersion()
{
return 0;
}
@Override
public int getEffectiveMajorVersion()
{
return 3;
}
@Override
public int getEffectiveMinorVersion()
{
return 0;
}
/**
* Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
*
* @param name
* The name of the servlet or JSP
* @return The dispatcher
*/
@Override
public RequestDispatcher getNamedDispatcher(final String name)
{
return getRequestDispatcher(name);
}
/**
* Get the real file path of the given resource name.
*
* @param name
* The name
* @return The real path or null
*/
@Override
public String getRealPath(String name)
{
try {
URL url = getResource(name);
if (url != null) {
// WICKET-6755 do not use url.getFile() as it does not properly decode the path
return new File(url.toURI()).getAbsolutePath();
}
} catch (IOException | URISyntaxException e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
*
* @param name
* The name of the resource to get the dispatcher for
* @return The dispatcher
*/
@Override
public RequestDispatcher getRequestDispatcher(final String name)
{
return new RequestDispatcher()
{
@Override
public void forward(ServletRequest servletRequest, ServletResponse servletResponse)
throws IOException
{
servletResponse.getWriter().write("FORWARD TO RESOURCE: " + name);
}
@Override
public void include(ServletRequest servletRequest, ServletResponse servletResponse)
throws IOException
{
servletResponse.getWriter().write("INCLUDE OF RESOURCE: " + name);
}
};
}
/**
* Get the URL for a particular resource that is relative to the web app root directory.
*
* @param name
* The name of the resource to get
* @return The resource, or null if resource not found
* @throws MalformedURLException
* If the URL is invalid
*/
@Override
public URL getResource(String name) throws MalformedURLException
{
if (name.startsWith("/"))
{
name = name.substring(1);
}
if (webappRoot != null)
{
File f = new File(webappRoot, name);
if (f.exists())
{
return f.toURI().toURL();
}
}
return getClass().getClassLoader().getResource("META-INF/resources/" + name);
}
/**
* Get an input stream for a particular resource that is relative to the web app root directory.
*
* @param name
* The name of the resource to get
* @return The input stream for the resource, or null of resource is not found
*/
@Override
public InputStream getResourceAsStream(String name)
{
try {
URL url = getResource(name);
if (url != null) {
return url.openStream();
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* Get the resource paths starting from the web app root directory and then relative to the the
* given name.
*
* @param name
* The starting name
* @return The set of resource paths at this location
*/
@Override
public Set<String> getResourcePaths(String name)
{
if (webappRoot == null)
{
return new HashSet<String>();
}
if (name.startsWith("/"))
{
name = name.substring(1);
}
if (name.endsWith("/"))
{
name = name.substring(0, name.length() - 1);
}
String[] elements = null;
if (name.trim().length() == 0)
{
elements = new String[0];
}
else
{
elements = Strings.split(name, '/');
}
File current = webappRoot;
for (String element : elements)
{
File[] files = current.listFiles();
boolean match = false;
if (files != null)
{
for (File file : files)
{
if (file.getName().equals(element) && file.isDirectory())
{
current = file;
match = true;
break;
}
}
}
if (!match)
{
return null;
}
}
File[] files = current.listFiles();
Set<String> result = new HashSet<>();
if (files != null)
{
int stripLength = webappRoot.getPath().length();
for (File file : files)
{
String s = file.getPath().substring(stripLength).replace('\\', '/');
if (file.isDirectory())
{
s = s + "/";
}
result.add(s);
}
}
return result;
}
/**
* Get the server info.
*
* @return The server info
*/
@Override
public String getServerInfo()
{
return "Wicket Mock Test Environment v1.0";
}
/**
* NOT USED - Servlet Spec requires that this always returns null.
*
* @param name
* Not used
* @return null
* @throws ServletException
* Not used
*/
@Override
public Servlet getServlet(String name) throws ServletException
{
return null;
}
/**
* Return the name of the servlet context.
*
* @return The name
*/
@Override
public String getServletContextName()
{
return application.getName();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, String className)
{
try
{
return addServlet(servletName, Class.forName(className).asSubclass(Servlet.class));
}
catch (ClassNotFoundException e)
{
throw new WicketRuntimeException(e);
}
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
{
Dynamic mockRegistration = (Dynamic)Proxy.newProxyInstance(Dynamic.class.getClassLoader(),
new Class<?>[]{Dynamic.class}, new MockedServletRegistationHandler(servletName));
servletRegistration.put(servletName, mockRegistration);
return mockRegistration;
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
{
try
{
return addServlet(servletName, servletClass.getDeclaredConstructor().newInstance());
}
catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
{
throw new WicketRuntimeException(e);
}
}
@Override
public Dynamic addJspFile(String s, String s1)
{
return null;
}
@Override
public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
{
try
{
return clazz.getDeclaredConstructor().newInstance();
}
catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
{
throw new WicketRuntimeException(e);
}
}
@Override
public ServletRegistration getServletRegistration(String servletName)
{
return servletRegistration.get(servletName);
}
@Override
public Map<String, ? extends ServletRegistration> getServletRegistrations()
{
return servletRegistration;
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, String className)
{
return null;
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
{
return null;
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
{
return null;
}
@Override
public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
{
return null;
}
@Override
public FilterRegistration getFilterRegistration(String filterName)
{
return null;
}
@Override
public Map<String, ? extends FilterRegistration> getFilterRegistrations()
{
return null;
}
@Override
public SessionCookieConfig getSessionCookieConfig()
{
return sessionCookieConfig;
}
@Override
public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
{
}
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
{
return EnumSet.of(SessionTrackingMode.COOKIE);
}
@Override
public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
{
return getDefaultSessionTrackingModes();
}
@Override
public void addListener(String className)
{
}
@Override
public <T extends EventListener> void addListener(T t)
{
}
@Override
public void addListener(Class<? extends EventListener> listenerClass)
{
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
{
return null;
}
@Override
public JspConfigDescriptor getJspConfigDescriptor()
{
return null;
}
@Override
public ClassLoader getClassLoader()
{
return null;
}
@Override
public void declareRoles(String... roleNames)
{
}
@Override
public String getVirtualServerName()
{
return "WicketTester 8.x";
}
@Override
public int getSessionTimeout()
{
return sessionTimeout;
}
@Override
public void setSessionTimeout(int sessionTimeout)
{
this.sessionTimeout = sessionTimeout;
}
@Override
public String getRequestCharacterEncoding()
{
return requestCharacterEncoding.name();
}
@Override
public void setRequestCharacterEncoding(String requestCharacterEncoding)
{
this.requestCharacterEncoding = Charset.forName(requestCharacterEncoding);
}
@Override
public String getResponseCharacterEncoding()
{
return responseCharacterEncoding.name();
}
@Override
public void setResponseCharacterEncoding(String responseCharacterEncoding)
{
this.responseCharacterEncoding = Charset.forName(responseCharacterEncoding);
}
/**
* NOT USED - Servlet spec requires that this always returns null.
*
* @return null
*/
@Override
public Enumeration<String> getServletNames()
{
return null;
}
/**
* NOT USED - Servlet spec requires that this always returns null.
*
* @return null
*/
@Override
public Enumeration<Servlet> getServlets()
{
return null;
}
/**
* As part of testing we always log to the console.
*
* @param e
* The exception to log
* @param msg
* The message to log
*/
@Override
public void log(Exception e, String msg)
{
log.error(msg, e);
}
/**
* As part of testing we always log to the console.
*
* @param msg
* The message to log
*/
@Override
public void log(String msg)
{
log.info(msg);
}
/**
* As part of testing we always log to the console.
*
* @param msg
* The message to log
* @param cause
* The cause exception
*/
@Override
public void log(String msg, Throwable cause)
{
log.error(msg, cause);
}
/**
* Remove an attribute with the given name.
*
* @param name
* The name
*/
@Override
public void removeAttribute(final String name)
{
attributes.remove(name);
}
/**
* Set an attribute.
*
* @param name
* The name of the attribute
* @param o
* The value
*/
@Override
public void setAttribute(final String name, final Object o)
{
attributes.put(name, o);
}
/**
* @return context path
*/
@Override
public String getContextPath()
{
return "";
}
/**
* Invocation handler for proxy interface of {@link jakarta.servlet.ServletRegistration.Dynamic}.
* This class intercepts invocation for method {@link jakarta.servlet.ServletRegistration.Dynamic#getMappings}
* and returns the servlet name.
*
* @author andrea del bene
*
*/
class MockedServletRegistationHandler implements InvocationHandler
{
private final Collection<String> servletName;
public MockedServletRegistationHandler(String servletName)
{
this.servletName = Arrays.asList(servletName);
}
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable
{
if (method.getName().equals("getMappings"))
{
return servletName;
}
return null;
}
}
}