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}