001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.protocol.http.mock;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.reflect.InvocationHandler;
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.Method;
025import java.lang.reflect.Proxy;
026import java.net.MalformedURLException;
027import java.net.URISyntaxException;
028import java.net.URL;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.EnumSet;
033import java.util.Enumeration;
034import java.util.EventListener;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.Map;
038import java.util.Set;
039
040import javax.servlet.Filter;
041import javax.servlet.FilterRegistration;
042import javax.servlet.RequestDispatcher;
043import javax.servlet.Servlet;
044import javax.servlet.ServletContext;
045import javax.servlet.ServletException;
046import javax.servlet.ServletRegistration;
047import javax.servlet.ServletRegistration.Dynamic;
048import javax.servlet.ServletRequest;
049import javax.servlet.ServletResponse;
050import javax.servlet.SessionCookieConfig;
051import javax.servlet.SessionTrackingMode;
052import javax.servlet.descriptor.JspConfigDescriptor;
053
054import org.apache.wicket.Application;
055import org.apache.wicket.WicketRuntimeException;
056import org.apache.wicket.util.cookies.CookieUtils;
057import org.apache.wicket.util.string.Strings;
058import org.apache.wicket.util.value.ValueMap;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062
063/**
064 * Mock implementation of the servlet context for testing purposes. This implementation supports all
065 * of the standard context methods except that request dispatching just indicates what is being
066 * dispatched to, rather than doing the actual dispatch.
067 * <p>
068 * The context can be configured with a path parameter that should point to an absolute directory
069 * location that represents the place where the contents of the WAR bundle are located. Setting this
070 * value allows all of the resource location functionality to work as in a fully functioning web
071 * application. This value is not set then not resource location functionality will work and instead
072 * null will always be returned.
073 *
074 * @author Chris Turner
075 */
076public class MockServletContext implements ServletContext
077{
078        private static final Logger log = LoggerFactory.getLogger(MockServletContext.class);
079
080        private final Application application;
081
082        private final ValueMap attributes = new ValueMap();
083
084        private final ValueMap initParameters = new ValueMap();
085
086        private final Map<String, ServletRegistration.Dynamic> servletRegistration = new HashMap<>();
087
088        /** Map of mime types */
089        private final ValueMap mimeTypes = new ValueMap();
090
091        private File webappRoot;
092
093        private final SessionCookieConfig sessionCookieConfig = new SessionCookieConfig()
094        {
095                private boolean secure;
096                private String path;
097                private String name = CookieUtils.DEFAULT_SESSIONID_COOKIE_NAME;
098                private int maxAge;
099                private boolean httpOnly;
100                private String domain;
101                private String comment;
102
103                @Override
104                public void setSecure(boolean secure)
105                {
106                        this.secure = secure;
107                }
108
109                @Override
110                public void setPath(String path)
111                {
112                        this.path = path;
113                }
114
115                @Override
116                public void setName(String name)
117                {
118                        this.name = name;
119                }
120
121                @Override
122                public void setMaxAge(int maxAge)
123                {
124                        this.maxAge = maxAge;
125                }
126
127                @Override
128                public void setHttpOnly(boolean httpOnly)
129                {
130                        this.httpOnly = httpOnly;
131                }
132
133                @Override
134                public void setDomain(String domain)
135                {
136                        this.domain = domain;
137                }
138
139                @Override
140                public void setComment(String comment)
141                {
142                        this.comment = comment;
143                }
144
145                @Override
146                public boolean isSecure()
147                {
148                        return secure;
149                }
150
151                @Override
152                public boolean isHttpOnly()
153                {
154                        return httpOnly;
155                }
156
157                @Override
158                public String getPath()
159                {
160                        return path;
161                }
162
163                @Override
164                public String getName()
165                {
166                        return name;
167                }
168
169                @Override
170                public int getMaxAge()
171                {
172                        return maxAge;
173                }
174
175                @Override
176                public String getDomain()
177                {
178                        return domain;
179                }
180
181                @Override
182                public String getComment()
183                {
184                        return comment;
185                }
186        };
187        /**
188         * Create the mock object. As part of the creation, the context sets the root directory where
189         * web application content is stored. This must be an ABSOLUTE directory relative to where the
190         * tests are being executed. For example: <code>System.getProperty("user.dir") +
191         * "/src/webapp"</code>
192         *
193         * @param application
194         *            The application that this context is for
195         * @param path
196         *            The path to the root of the web application
197         */
198        public MockServletContext(final Application application, final String path)
199        {
200                this.application = application;
201
202                webappRoot = null;
203                if (path != null)
204                {
205                        webappRoot = new File(path);
206                        if (!webappRoot.exists() || !webappRoot.isDirectory())
207                        {
208                                log.warn("WARNING: The webapp root directory is invalid: " + path);
209                                webappRoot = null;
210                        }
211                }
212
213                // the user app can configure specific work folder by setting -Dwicket.tester.work.folder JVM option,
214                // otherwise assume we're running in maven or an eclipse project created by maven,
215                // so the sessions directory will be created inside the target directory,
216                // and will be cleaned up with a mvn clean
217
218                String workFolder = System.getProperty("wicket.tester.work.folder", "target/work/");
219                File file = new File(workFolder);
220                try
221                {
222                        file.mkdirs();
223                }
224                catch (SecurityException sx)
225                {
226                        // not allowed to write so fallback to tmpdir
227                        String tmpDir = System.getProperty("java.io.tmpdir");
228                        file = new File(tmpDir);
229                }
230
231                attributes.put("javax.servlet.context.tempdir", file);
232
233                mimeTypes.put("html", "text/html");
234                mimeTypes.put("htm", "text/html");
235                mimeTypes.put("css", "text/css");
236                mimeTypes.put("xml", "text/xml");
237                mimeTypes.put("js", "text/javascript");
238                mimeTypes.put("gif", "image/gif");
239                mimeTypes.put("jpg", "image/jpeg");
240                mimeTypes.put("png", "image/png");
241        }
242
243        /**
244         * Add an init parameter.
245         *
246         * @param name
247         *            The parameter name
248         * @param value
249         *            The parameter value
250         */
251        public void addInitParameter(final String name, final String value)
252        {
253                initParameters.put(name, value);
254        }
255
256        // Configuration methods
257
258        /**
259         * Add a new recognized mime type.
260         *
261         * @param fileExtension
262         *            The file extension (e.g. "jpg")
263         * @param mimeType
264         *            The mime type (e.g. "image/jpeg")
265         */
266        public void addMimeType(final String fileExtension, final String mimeType)
267        {
268                mimeTypes.put(fileExtension, mimeType);
269        }
270
271        /**
272         * Get an attribute with the given name.
273         *
274         * @param name
275         *            The attribute name
276         * @return The value, or null
277         */
278        @Override
279        public Object getAttribute(final String name)
280        {
281                return attributes.get(name);
282        }
283
284        /**
285         * Get all of the attribute names.
286         *
287         * @return The attribute names
288         */
289        @Override
290        public Enumeration<String> getAttributeNames()
291        {
292                return Collections.enumeration(attributes.keySet());
293        }
294
295        // ServletContext interface methods
296
297        /**
298         * Get the context for the given URL path
299         *
300         * @param name
301         *            The url path
302         * @return Always returns this
303         */
304        @Override
305        public ServletContext getContext(String name)
306        {
307                return this;
308        }
309
310        /**
311         * Get the init parameter with the given name.
312         *
313         * @param name
314         *            The name
315         * @return The parameter, or null if no such parameter
316         */
317        @Override
318        public String getInitParameter(final String name)
319        {
320                return initParameters.getString(name);
321        }
322
323        /**
324         * Get the name of all of the init parameters.
325         *
326         * @return The init parameter names
327         */
328        @Override
329        public Enumeration<String> getInitParameterNames()
330        {
331                return Collections.enumeration(initParameters.keySet());
332        }
333
334        @Override
335        public boolean setInitParameter(String name, String value)
336        {
337                return false;
338        }
339
340        /**
341         * Get the mime type for the given file. Uses a hardcoded map of mime types set at
342         * Initialization time.
343         *
344         * @param name
345         *            The name to get the mime type for
346         * @return The mime type
347         */
348        @Override
349        public String getMimeType(final String name)
350        {
351                int index = name.lastIndexOf('.');
352                if (index == -1 || index == (name.length() - 1))
353                {
354                        return null;
355                }
356                else
357                {
358                        return mimeTypes.getString(name.substring(index + 1));
359                }
360        }
361
362        @Override
363        public int getMajorVersion()
364        {
365                return 3;
366        }
367
368        @Override
369        public int getMinorVersion()
370        {
371                return 0;
372        }
373
374        @Override
375        public int getEffectiveMajorVersion()
376        {
377                return 3;
378        }
379
380        @Override
381        public int getEffectiveMinorVersion()
382        {
383                return 0;
384        }
385
386        /**
387         * Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
388         *
389         * @param name
390         *            The name of the servlet or JSP
391         * @return The dispatcher
392         */
393        @Override
394        public RequestDispatcher getNamedDispatcher(final String name)
395        {
396                return getRequestDispatcher(name);
397        }
398
399        /**
400         * Get the real file path of the given resource name.
401         *
402         * @param name
403         *            The name
404         * @return The real path or null
405         */
406        @Override
407        public String getRealPath(String name)
408        {
409                try {
410                        URL url = getResource(name);
411                        if (url != null) {
412                                // WICKET-6755 do not use url.getFile() as it does not properly decode the path
413                                return new File(url.toURI()).getAbsolutePath();
414                        }
415                } catch (IOException | URISyntaxException e) {
416                        log.error(e.getMessage(), e);
417                }
418                return null;
419        }
420
421        /**
422         * Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
423         *
424         * @param name
425         *            The name of the resource to get the dispatcher for
426         * @return The dispatcher
427         */
428        @Override
429        public RequestDispatcher getRequestDispatcher(final String name)
430        {
431                return new RequestDispatcher()
432                {
433                        @Override
434                        public void forward(ServletRequest servletRequest, ServletResponse servletResponse)
435                                throws IOException
436                        {
437                                servletResponse.getWriter().write("FORWARD TO RESOURCE: " + name);
438                        }
439
440                        @Override
441                        public void include(ServletRequest servletRequest, ServletResponse servletResponse)
442                                throws IOException
443                        {
444                                servletResponse.getWriter().write("INCLUDE OF RESOURCE: " + name);
445                        }
446                };
447        }
448
449        /**
450         * Get the URL for a particular resource that is relative to the web app root directory.
451         *
452         * @param name
453         *            The name of the resource to get
454         * @return The resource, or null if resource not found
455         * @throws MalformedURLException
456         *             If the URL is invalid
457         */
458        @Override
459        public URL getResource(String name) throws MalformedURLException
460        {
461                if (name.startsWith("/"))
462                {
463                        name = name.substring(1);
464                }
465
466                if (webappRoot != null)
467                {
468                        File f = new File(webappRoot, name);
469                        if (f.exists())
470                        {
471                                return f.toURI().toURL();
472                        }
473                }
474
475                return getClass().getClassLoader().getResource("META-INF/resources/" + name);
476        }
477
478        /**
479         * Get an input stream for a particular resource that is relative to the web app root directory.
480         *
481         * @param name
482         *            The name of the resource to get
483         * @return The input stream for the resource, or null of resource is not found
484         */
485        @Override
486        public InputStream getResourceAsStream(String name)
487        {
488                try {
489                        URL url = getResource(name);
490                        if (url != null) {
491                                return url.openStream();
492                        }
493                } catch (IOException e) {
494                        log.error(e.getMessage(), e);
495                }
496                return null;
497        }
498
499        /**
500         * Get the resource paths starting from the web app root directory and then relative to the the
501         * given name.
502         *
503         * @param name
504         *            The starting name
505         * @return The set of resource paths at this location
506         */
507        @Override
508        public Set<String> getResourcePaths(String name)
509        {
510                if (webappRoot == null)
511                {
512                        return new HashSet<String>();
513                }
514
515                if (name.startsWith("/"))
516                {
517                        name = name.substring(1);
518                }
519                if (name.endsWith("/"))
520                {
521                        name = name.substring(0, name.length() - 1);
522                }
523                String[] elements = null;
524                if (name.trim().length() == 0)
525                {
526                        elements = new String[0];
527                }
528                else
529                {
530                        elements = Strings.split(name, '/');
531                }
532
533                File current = webappRoot;
534                for (String element : elements)
535                {
536                        File[] files = current.listFiles();
537                        boolean match = false;
538                        if (files != null)
539                        {
540                                for (File file : files)
541                                {
542                                        if (file.getName().equals(element) && file.isDirectory())
543                                        {
544                                                current = file;
545                                                match = true;
546                                                break;
547                                        }
548                                }
549                        }
550                        if (!match)
551                        {
552                                return null;
553                        }
554                }
555
556                File[] files = current.listFiles();
557                Set<String> result = new HashSet<>();
558                if (files != null)
559                {
560                        int stripLength = webappRoot.getPath().length();
561                        for (File file : files)
562                        {
563                                String s = file.getPath().substring(stripLength).replace('\\', '/');
564                                if (file.isDirectory())
565                                {
566                                        s = s + "/";
567                                }
568                                result.add(s);
569                        }
570                }
571                return result;
572        }
573
574        /**
575         * Get the server info.
576         *
577         * @return The server info
578         */
579        @Override
580        public String getServerInfo()
581        {
582                return "Wicket Mock Test Environment v1.0";
583        }
584
585        /**
586         * NOT USED - Servlet Spec requires that this always returns null.
587         *
588         * @param name
589         *            Not used
590         * @return null
591         * @throws ServletException
592         *             Not used
593         */
594        @Override
595        public Servlet getServlet(String name) throws ServletException
596        {
597                return null;
598        }
599
600        /**
601         * Return the name of the servlet context.
602         *
603         * @return The name
604         */
605        @Override
606        public String getServletContextName()
607        {
608                return application.getName();
609        }
610
611        @Override
612        public ServletRegistration.Dynamic addServlet(String servletName, String className)
613        {
614                try
615                {
616                        return addServlet(servletName, Class.forName(className).asSubclass(Servlet.class));
617                }
618                catch (ClassNotFoundException e)
619                {
620                        throw new WicketRuntimeException(e);
621                }
622        }
623
624        @Override
625        public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
626        {
627                Dynamic mockRegistration = (Dynamic)Proxy.newProxyInstance(Dynamic.class.getClassLoader(),
628                        new Class<?>[]{Dynamic.class}, new MockedServletRegistationHandler(servletName));
629
630                servletRegistration.put(servletName, mockRegistration);
631
632                return mockRegistration;
633        }
634
635        @Override
636        public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
637        {
638                try
639                {
640                        return addServlet(servletName, servletClass.getDeclaredConstructor().newInstance());
641                }
642                catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
643                {
644                        throw new WicketRuntimeException(e);
645                }
646        }
647
648        @Override
649        public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
650        {
651                try
652                {
653                        return clazz.getDeclaredConstructor().newInstance();
654                }
655                catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
656                {
657                        throw new WicketRuntimeException(e);
658                }
659        }
660
661        @Override
662        public ServletRegistration getServletRegistration(String servletName)
663        {
664                return servletRegistration.get(servletName);
665        }
666
667        @Override
668        public Map<String, ? extends ServletRegistration> getServletRegistrations()
669        {
670                return servletRegistration;
671        }
672
673        @Override
674        public FilterRegistration.Dynamic addFilter(String filterName, String className)
675        {
676                return null;
677        }
678
679        @Override
680        public FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
681        {
682                return null;
683        }
684
685        @Override
686        public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
687        {
688                return null;
689        }
690
691        @Override
692        public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
693        {
694                return null;
695        }
696
697        @Override
698        public FilterRegistration getFilterRegistration(String filterName)
699        {
700                return null;
701        }
702
703        @Override
704        public Map<String, ? extends FilterRegistration> getFilterRegistrations()
705        {
706                return null;
707        }
708
709        @Override
710        public SessionCookieConfig getSessionCookieConfig()
711        {
712                return sessionCookieConfig;
713        }
714
715        @Override
716        public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
717        {
718        }
719
720        @Override
721        public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
722        {
723                return EnumSet.of(SessionTrackingMode.COOKIE);
724        }
725
726        @Override
727        public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
728        {
729                return getDefaultSessionTrackingModes();
730        }
731
732        @Override
733        public void addListener(String className)
734        {
735        }
736
737        @Override
738        public <T extends EventListener> void addListener(T t)
739        {
740        }
741
742        @Override
743        public void addListener(Class<? extends EventListener> listenerClass)
744        {
745        }
746
747        @Override
748        public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
749        {
750                return null;
751        }
752
753        @Override
754        public JspConfigDescriptor getJspConfigDescriptor()
755        {
756                return null;
757        }
758
759        @Override
760        public ClassLoader getClassLoader()
761        {
762                return null;
763        }
764
765        @Override
766        public void declareRoles(String... roleNames)
767        {
768        }
769
770        @Override
771        public String getVirtualServerName()
772        {
773                return "WicketTester 8.x";
774        }
775
776        /**
777         * NOT USED - Servlet spec requires that this always returns null.
778         *
779         * @return null
780         */
781        @Override
782        public Enumeration<String> getServletNames()
783        {
784                return null;
785        }
786
787        /**
788         * NOT USED - Servlet spec requires that this always returns null.
789         *
790         * @return null
791         */
792        @Override
793        public Enumeration<Servlet> getServlets()
794        {
795                return null;
796        }
797
798        /**
799         * As part of testing we always log to the console.
800         *
801         * @param e
802         *            The exception to log
803         * @param msg
804         *            The message to log
805         */
806        @Override
807        public void log(Exception e, String msg)
808        {
809                log.error(msg, e);
810        }
811
812        /**
813         * As part of testing we always log to the console.
814         *
815         * @param msg
816         *            The message to log
817         */
818        @Override
819        public void log(String msg)
820        {
821                log.info(msg);
822        }
823
824        /**
825         * As part of testing we always log to the console.
826         *
827         * @param msg
828         *            The message to log
829         * @param cause
830         *            The cause exception
831         */
832        @Override
833        public void log(String msg, Throwable cause)
834        {
835                log.error(msg, cause);
836        }
837
838        /**
839         * Remove an attribute with the given name.
840         *
841         * @param name
842         *            The name
843         */
844        @Override
845        public void removeAttribute(final String name)
846        {
847                attributes.remove(name);
848        }
849
850        /**
851         * Set an attribute.
852         *
853         * @param name
854         *            The name of the attribute
855         * @param o
856         *            The value
857         */
858        @Override
859        public void setAttribute(final String name, final Object o)
860        {
861                attributes.put(name, o);
862        }
863
864        /**
865         * @return context path
866         */
867        @Override
868        public String getContextPath()
869        {
870                return "";
871        }
872
873
874        /**
875         * Invocation handler for proxy interface of {@link javax.servlet.ServletRegistration.Dynamic}.
876         * This class intercepts invocation for method {@link javax.servlet.ServletRegistration.Dynamic#getMappings}
877         * and returns the servlet name.
878         *
879         * @author andrea del bene
880         *
881         */
882        class MockedServletRegistationHandler implements InvocationHandler
883        {
884
885                private final Collection<String> servletName;
886
887                public MockedServletRegistationHandler(String servletName)
888                {
889                        this.servletName = Arrays.asList(servletName);
890                }
891
892                @Override
893                public Object invoke(Object object, Method method, Object[] args) throws Throwable
894                {
895                        if (method.getName().equals("getMappings"))
896                        {
897                                return servletName;
898                        }
899
900                        return null;
901                }
902        }
903}